@ledgerhq/device-transport-kit-speculos 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/lib/cjs/package.json +2 -3
- package/lib/cjs/src/api/SpeculosTransport.js +1 -1
- package/lib/cjs/src/api/SpeculosTransport.js.map +3 -3
- package/lib/cjs/src/api/SpeculosTransport.test.js +2 -0
- package/lib/cjs/src/api/SpeculosTransport.test.js.map +7 -0
- package/lib/cjs/src/internal/datasource/HttpSpeculosDatasource.js +1 -1
- package/lib/cjs/src/internal/datasource/HttpSpeculosDatasource.js.map +3 -3
- package/lib/cjs/src/internal/datasource/HttpSpeculosDatasource.test.js +4 -0
- package/lib/cjs/src/internal/datasource/HttpSpeculosDatasource.test.js.map +7 -0
- package/lib/esm/package.json +2 -3
- package/lib/esm/src/api/SpeculosTransport.js +1 -1
- package/lib/esm/src/api/SpeculosTransport.js.map +3 -3
- package/lib/esm/src/api/SpeculosTransport.test.js +2 -0
- package/lib/esm/src/api/SpeculosTransport.test.js.map +7 -0
- package/lib/esm/src/internal/datasource/HttpSpeculosDatasource.js +1 -1
- package/lib/esm/src/internal/datasource/HttpSpeculosDatasource.js.map +3 -3
- package/lib/esm/src/internal/datasource/HttpSpeculosDatasource.test.js +4 -0
- package/lib/esm/src/internal/datasource/HttpSpeculosDatasource.test.js.map +7 -0
- package/lib/types/src/api/SpeculosTransport.d.ts +6 -2
- package/lib/types/src/api/SpeculosTransport.d.ts.map +1 -1
- package/lib/types/src/api/SpeculosTransport.test.d.ts +2 -0
- package/lib/types/src/api/SpeculosTransport.test.d.ts.map +1 -0
- package/lib/types/src/internal/datasource/HttpSpeculosDatasource.d.ts +3 -4
- package/lib/types/src/internal/datasource/HttpSpeculosDatasource.d.ts.map +1 -1
- package/lib/types/src/internal/datasource/HttpSpeculosDatasource.test.d.ts +2 -0
- package/lib/types/src/internal/datasource/HttpSpeculosDatasource.test.d.ts.map +1 -0
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -42,7 +42,10 @@ To use this transport, ensure you have the Device Management Kit installed in yo
|
|
|
42
42
|
To use the transport, you need to inject it in the DeviceManagementKitBuilder before the build. This will allow the Device Management Kit to find and interact with speculos devices trhough http
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
DeviceManagementKitBuilder,
|
|
47
|
+
DeviceModelId,
|
|
48
|
+
} from "@ledgerhq/device-management-kit";
|
|
46
49
|
import {
|
|
47
50
|
speculosTransportFactory,
|
|
48
51
|
SpeculosTransport,
|
|
@@ -54,6 +57,18 @@ const dmk = new DeviceManagementKitBuilder()
|
|
|
54
57
|
.addTransport(speculosTransportFactory("http://localhost:5001")) // With custom server
|
|
55
58
|
.build();
|
|
56
59
|
|
|
60
|
+
// With a bearer token sent on every request (`Authorization: Bearer <token>`), e.g. a protected Speculinho session
|
|
61
|
+
const dmk = new DeviceManagementKitBuilder()
|
|
62
|
+
.addTransport(
|
|
63
|
+
speculosTransportFactory(
|
|
64
|
+
"https://my-run.speculos.ledgerlabs.net",
|
|
65
|
+
false,
|
|
66
|
+
DeviceModelId.NANO_X,
|
|
67
|
+
{ bearerToken: sessionToken },
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
.build();
|
|
71
|
+
|
|
57
72
|
// With custom config
|
|
58
73
|
const dmk = new DeviceManagementKitBuilder()
|
|
59
74
|
.addTransport(
|
package/lib/cjs/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@sentry/minimal": "catalog:",
|
|
4
|
-
"axios": "catalog:",
|
|
5
4
|
"purify-ts": "catalog:"
|
|
6
5
|
},
|
|
7
6
|
"devDependencies": {
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
"license": "Apache-2.0",
|
|
33
32
|
"name": "@ledgerhq/device-transport-kit-speculos",
|
|
34
33
|
"peerDependencies": {
|
|
35
|
-
"@ledgerhq/device-management-kit": "
|
|
34
|
+
"@ledgerhq/device-management-kit": "^1.11.0",
|
|
36
35
|
"rxjs": "catalog:"
|
|
37
36
|
},
|
|
38
37
|
"private": false,
|
|
@@ -56,5 +55,5 @@
|
|
|
56
55
|
"watch:builds": "pnpm ldmk-tool watch --entryPoints index.ts,src/**/*.ts --tsconfig tsconfig.prod.json",
|
|
57
56
|
"watch:types": "concurrently \"tsc --watch -p tsconfig.prod.json\" \"tsc-alias --watch -p tsconfig.prod.json\""
|
|
58
57
|
},
|
|
59
|
-
"version": "1.
|
|
58
|
+
"version": "1.3.0"
|
|
60
59
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var l=Object.defineProperty;var
|
|
1
|
+
"use strict";var l=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var b=(s,e)=>{for(var t in e)l(s,t,{get:e[t],enumerable:!0})},y=(s,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of f(e))!m.call(s,r)&&r!==t&&l(s,r,{get:()=>e[r],enumerable:!(o=S(e,r))||o.enumerable});return s};var I=s=>y(l({},"__esModule",{value:!0}),s);var A={};b(A,{SpeculosTransport:()=>D,speculosIdentifier:()=>g,speculosTransportFactory:()=>T});module.exports=I(A);var n=require("@ledgerhq/device-management-kit"),c=require("purify-ts"),u=require("rxjs"),v=require("../internal/datasource/HttpSpeculosDatasource");const g="SPECULOS_HTTP_TRANSPORT";class D{logger;identifier=g;_speculosDataSource;connectedDevice=null;disconnectInterval=null;_isE2E;speculosDevice;constructor(e,t,o,r,i=n.DeviceModelId.STAX,{bearerToken:a}={}){this._isE2E=r??!1,this.logger=e("SpeculosTransport"),this._speculosDataSource=new v.HttpSpeculosDatasource(o,void 0,a),this.speculosDevice={id:"SpeculosID",deviceModel:{id:i,productName:"Speculos - App Name - version",usbProductId:16,bootloaderUsbProductId:1,getBlockSize(){return 32},usbOnly:!0,memorySize:320*1024,masks:[823132160]},transport:this.identifier}}isSupported(){return!0}getIdentifier(){return this.identifier}listenToAvailableDevices(){return(0,u.from)([[this.speculosDevice]])}startDiscovering(){return this.logger.debug("startDiscovering"),(0,u.from)([this.speculosDevice])}stopDiscovering(){this.logger.debug("stopDiscovering")}async connect(e){this.logger.debug("connect");const t=await this._speculosDataSource.postApdu("B0010000");this.logger.debug(`Hex Response: ${t}`);const o=this.createApduResponse(t),r=new n.ApduParser(o);r.extract8BitUInt();const i=r.encodeToString(r.extractFieldLVEncoded()),a=r.encodeToString(r.extractFieldLVEncoded());this.logger.debug(`App Name: ${i} and version ${a}`);const p=e.deviceId;try{const d={sendApdu:h=>this.sendApdu(p,e.deviceId,e.onDisconnect,h),deviceModel:{...this.speculosDevice.deviceModel,productName:`Speculos - ${i} - ${a}`,getBlockSize(){return 32}},transport:this.identifier,id:"SpeculosID",type:"USB"};return this.connectedDevice=d,this._isE2E||this.listenForDisconnect(e.onDisconnect,e.deviceId),(0,c.Right)(d)}catch(d){return(0,c.Left)(new n.OpeningConnectionError(d))}}async disconnect(e){return this.logger.debug("disconnect"),this.connectedDevice=null,this.clearDisconnectInterval(),Promise.resolve((0,c.Right)(void 0))}async sendApdu(e,t,o,r){try{const i=(0,n.bufferToHexaString)(r).substring(2),a=await this._speculosDataSource.postApdu(i);this.logger.debug((0,n.formatApduSentLog)(r));const p=this.createApduResponse(a);return this.logger.debug((0,n.formatApduReceivedLog)(p)),(0,c.Right)(p)}catch(i){return this.connectedDevice&&(this.logger.debug("disconnecting"),o(t),this.disconnect({connectedDevice:this.connectedDevice})),(0,c.Left)(new n.GeneralDmkError(i))}}createApduResponse(e){return{statusCode:this.fromHexString(e.substring(e.length-4,e.length)),data:this.fromHexString(e.substring(0,e.length-4))}}fromHexString(e){return e?new Uint8Array(e.match(/.{1,2}/g).map(t=>parseInt(t,16))):Uint8Array.from([])}listenForDisconnect(e,t){this.disconnectInterval=setInterval(async()=>{await this._speculosDataSource.isServerAvailable()||(this.logger.info(`Speculos server unavailable, disconnecting device ${t}`),e(t),this.connectedDevice&&await this.disconnect({connectedDevice:this.connectedDevice}),this.clearDisconnectInterval())},2e3)}clearDisconnectInterval(){this.disconnectInterval&&(clearInterval(this.disconnectInterval),this.disconnectInterval=null)}}const T=(s="http://127.0.0.1:5000",e=!1,t,o)=>({config:r,loggerServiceFactory:i})=>new D(i,r,s,e,t,o);0&&(module.exports={SpeculosTransport,speculosIdentifier,speculosTransportFactory});
|
|
2
2
|
//# sourceMappingURL=SpeculosTransport.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/api/SpeculosTransport.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n ApduParser,\n type ApduResponse,\n bufferToHexaString,\n type ConnectError,\n type DeviceId,\n DeviceModelId,\n type DisconnectHandler,\n type DmkConfig,\n type DmkError,\n formatApduReceivedLog,\n formatApduSentLog,\n GeneralDmkError,\n type LoggerPublisherService,\n OpeningConnectionError,\n type Transport,\n type TransportConnectedDevice,\n type TransportDiscoveredDevice,\n type TransportFactory,\n type TransportIdentifier,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Right } from \"purify-ts\";\nimport { from, type Observable } from \"rxjs\";\n\nimport { HttpSpeculosDatasource } from \"@internal/datasource/HttpSpeculosDatasource\";\nimport { type SpeculosDatasource } from \"@internal/datasource/SpeculosDatasource\";\n\nexport const speculosIdentifier: TransportIdentifier =\n \"SPECULOS_HTTP_TRANSPORT\";\n\nexport class SpeculosTransport implements Transport {\n private logger: LoggerPublisherService;\n private readonly identifier: TransportIdentifier = speculosIdentifier;\n private readonly _speculosDataSource: SpeculosDatasource;\n private connectedDevice: TransportConnectedDevice | null = null;\n private disconnectInterval: NodeJS.Timeout | null = null;\n private readonly _isE2E: boolean;\n private readonly speculosDevice: TransportDiscoveredDevice;\n\n constructor(\n loggerServiceFactory: (tag: string) => LoggerPublisherService,\n _config: DmkConfig,\n speculosUrl: string,\n isE2E?: boolean,\n deviceModelId: DeviceModelId = DeviceModelId.STAX,\n ) {\n this._isE2E = isE2E ?? false;\n this.logger = loggerServiceFactory(\"SpeculosTransport\");\n this._speculosDataSource = new HttpSpeculosDatasource(speculosUrl);\n this.speculosDevice = {\n id: \"SpeculosID\",\n deviceModel: {\n id: deviceModelId,\n productName: \"Speculos - App Name - version\",\n usbProductId: 0x10,\n bootloaderUsbProductId: 0x0001,\n getBlockSize() {\n return 32;\n },\n usbOnly: true,\n memorySize: 320 * 1024,\n masks: [0x31100000],\n },\n transport: this.identifier,\n };\n }\n\n isSupported(): boolean {\n return true;\n }\n\n getIdentifier(): TransportIdentifier {\n return this.identifier;\n }\n\n listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]> {\n return from([[this.speculosDevice]]);\n }\n\n startDiscovering(): Observable<TransportDiscoveredDevice> {\n this.logger.debug(\"startDiscovering\");\n return from([this.speculosDevice]);\n }\n\n stopDiscovering(): void {\n //DO NOTHING HERE\n this.logger.debug(\"stopDiscovering\");\n }\n\n async connect(params: {\n deviceId: DeviceId;\n onDisconnect: DisconnectHandler;\n }): Promise<Either<ConnectError, TransportConnectedDevice>> {\n this.logger.debug(\"connect\");\n\n const hexResponse = await this._speculosDataSource.postApdu(\"B0010000\");\n this.logger.debug(`Hex Response: ${hexResponse}`);\n const apduResponse = this.createApduResponse(hexResponse);\n const parser = new ApduParser(apduResponse);\n\n //Copy paste from GetAppAndVersionCommand\n parser.extract8BitUInt(); //Need otherwise parser is not in the right position\n const appName = parser.encodeToString(parser.extractFieldLVEncoded());\n const appVersion = parser.encodeToString(parser.extractFieldLVEncoded());\n\n this.logger.debug(`App Name: ${appName} and version ${appVersion}`);\n\n const sessionId: string = params.deviceId;\n try {\n const connectedDevice: TransportConnectedDevice = {\n sendApdu: (apdu) => {\n return this.sendApdu(\n sessionId,\n params.deviceId,\n params.onDisconnect,\n apdu,\n );\n },\n deviceModel: {\n ...this.speculosDevice.deviceModel,\n productName: `Speculos - ${appName} - ${appVersion}`,\n getBlockSize() {\n return 32;\n },\n },\n transport: this.identifier,\n id: \"SpeculosID\", //TODO make it dynamic at creation\n type: \"USB\",\n };\n\n this.connectedDevice = connectedDevice;\n if (!this._isE2E) {\n this.listenForDisconnect(params.onDisconnect, params.deviceId);\n }\n return Right(connectedDevice);\n } catch (error) {\n return Left(new OpeningConnectionError(error as Error));\n }\n }\n\n async disconnect(_params: {\n connectedDevice: TransportConnectedDevice;\n }): Promise<Either<DmkError, void>> {\n this.logger.debug(\"disconnect\");\n this.connectedDevice = null;\n return Promise.resolve(Right(undefined));\n }\n\n async sendApdu(\n _sessionId: string,\n deviceId: DeviceId,\n onDisconnect: DisconnectHandler,\n apdu: Uint8Array,\n ): Promise<Either<DmkError, ApduResponse>> {\n try {\n const hexApdu = bufferToHexaString(apdu).substring(2);\n const hexResponse: string =\n await this._speculosDataSource.postApdu(hexApdu);\n this.logger.debug(formatApduSentLog(apdu));\n const apduResponse = this.createApduResponse(hexResponse);\n this.logger.debug(formatApduReceivedLog(apduResponse));\n return Right(apduResponse);\n } catch (error) {\n if (this.connectedDevice) {\n this.logger.debug(\"disconnecting\");\n onDisconnect(deviceId);\n this.disconnect({\n connectedDevice: this.connectedDevice,\n });\n
|
|
5
|
-
"mappings": "yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,uBAAAE,EAAA,uBAAAC,EAAA,6BAAAC,IAAA,eAAAC,EAAAL,GAAA,IAAAM,EAoBO,2CACPC,EAAyC,qBACzCC,EAAsC,gBAEtCC,EAAuC,uDAGhC,MAAMN,EACX,
|
|
6
|
-
"names": ["SpeculosTransport_exports", "__export", "SpeculosTransport", "speculosIdentifier", "speculosTransportFactory", "__toCommonJS", "import_device_management_kit", "import_purify_ts", "import_rxjs", "import_HttpSpeculosDatasource", "loggerServiceFactory", "_config", "speculosUrl", "isE2E", "deviceModelId", "params", "hexResponse", "apduResponse", "parser", "appName", "appVersion", "sessionId", "connectedDevice", "apdu", "error", "_params", "_sessionId", "deviceId", "onDisconnect", "hexApdu", "hexString", "byte", "config"]
|
|
4
|
+
"sourcesContent": ["import {\n ApduParser,\n type ApduResponse,\n bufferToHexaString,\n type ConnectError,\n type DeviceId,\n DeviceModelId,\n type DisconnectHandler,\n type DmkConfig,\n type DmkError,\n formatApduReceivedLog,\n formatApduSentLog,\n GeneralDmkError,\n type LoggerPublisherService,\n OpeningConnectionError,\n type Transport,\n type TransportConnectedDevice,\n type TransportDiscoveredDevice,\n type TransportFactory,\n type TransportIdentifier,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Right } from \"purify-ts\";\nimport { from, type Observable } from \"rxjs\";\n\nimport { HttpSpeculosDatasource } from \"@internal/datasource/HttpSpeculosDatasource\";\nimport { type SpeculosDatasource } from \"@internal/datasource/SpeculosDatasource\";\n\nexport const speculosIdentifier: TransportIdentifier =\n \"SPECULOS_HTTP_TRANSPORT\";\n\nexport type SpeculosTransportOptions = {\n bearerToken?: string;\n};\n\nexport class SpeculosTransport implements Transport {\n private logger: LoggerPublisherService;\n private readonly identifier: TransportIdentifier = speculosIdentifier;\n private readonly _speculosDataSource: SpeculosDatasource;\n private connectedDevice: TransportConnectedDevice | null = null;\n private disconnectInterval: NodeJS.Timeout | null = null;\n private readonly _isE2E: boolean;\n private readonly speculosDevice: TransportDiscoveredDevice;\n\n constructor(\n loggerServiceFactory: (tag: string) => LoggerPublisherService,\n _config: DmkConfig,\n speculosUrl: string,\n isE2E?: boolean,\n deviceModelId: DeviceModelId = DeviceModelId.STAX,\n { bearerToken }: SpeculosTransportOptions = {},\n ) {\n this._isE2E = isE2E ?? false;\n this.logger = loggerServiceFactory(\"SpeculosTransport\");\n this._speculosDataSource = new HttpSpeculosDatasource(\n speculosUrl,\n undefined,\n bearerToken,\n );\n this.speculosDevice = {\n id: \"SpeculosID\",\n deviceModel: {\n id: deviceModelId,\n productName: \"Speculos - App Name - version\",\n usbProductId: 0x10,\n bootloaderUsbProductId: 0x0001,\n getBlockSize() {\n return 32;\n },\n usbOnly: true,\n memorySize: 320 * 1024,\n masks: [0x31100000],\n },\n transport: this.identifier,\n };\n }\n\n isSupported(): boolean {\n return true;\n }\n\n getIdentifier(): TransportIdentifier {\n return this.identifier;\n }\n\n listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]> {\n return from([[this.speculosDevice]]);\n }\n\n startDiscovering(): Observable<TransportDiscoveredDevice> {\n this.logger.debug(\"startDiscovering\");\n return from([this.speculosDevice]);\n }\n\n stopDiscovering(): void {\n //DO NOTHING HERE\n this.logger.debug(\"stopDiscovering\");\n }\n\n async connect(params: {\n deviceId: DeviceId;\n onDisconnect: DisconnectHandler;\n }): Promise<Either<ConnectError, TransportConnectedDevice>> {\n this.logger.debug(\"connect\");\n\n const hexResponse = await this._speculosDataSource.postApdu(\"B0010000\");\n this.logger.debug(`Hex Response: ${hexResponse}`);\n const apduResponse = this.createApduResponse(hexResponse);\n const parser = new ApduParser(apduResponse);\n\n //Copy paste from GetAppAndVersionCommand\n parser.extract8BitUInt(); //Need otherwise parser is not in the right position\n const appName = parser.encodeToString(parser.extractFieldLVEncoded());\n const appVersion = parser.encodeToString(parser.extractFieldLVEncoded());\n\n this.logger.debug(`App Name: ${appName} and version ${appVersion}`);\n\n const sessionId: string = params.deviceId;\n try {\n const connectedDevice: TransportConnectedDevice = {\n sendApdu: (apdu) => {\n return this.sendApdu(\n sessionId,\n params.deviceId,\n params.onDisconnect,\n apdu,\n );\n },\n deviceModel: {\n ...this.speculosDevice.deviceModel,\n productName: `Speculos - ${appName} - ${appVersion}`,\n getBlockSize() {\n return 32;\n },\n },\n transport: this.identifier,\n id: \"SpeculosID\", //TODO make it dynamic at creation\n type: \"USB\",\n };\n\n this.connectedDevice = connectedDevice;\n if (!this._isE2E) {\n this.listenForDisconnect(params.onDisconnect, params.deviceId);\n }\n return Right(connectedDevice);\n } catch (error) {\n return Left(new OpeningConnectionError(error as Error));\n }\n }\n\n async disconnect(_params: {\n connectedDevice: TransportConnectedDevice;\n }): Promise<Either<DmkError, void>> {\n this.logger.debug(\"disconnect\");\n this.connectedDevice = null;\n this.clearDisconnectInterval();\n return Promise.resolve(Right(undefined));\n }\n\n async sendApdu(\n _sessionId: string,\n deviceId: DeviceId,\n onDisconnect: DisconnectHandler,\n apdu: Uint8Array,\n ): Promise<Either<DmkError, ApduResponse>> {\n try {\n const hexApdu = bufferToHexaString(apdu).substring(2);\n const hexResponse: string =\n await this._speculosDataSource.postApdu(hexApdu);\n this.logger.debug(formatApduSentLog(apdu));\n const apduResponse = this.createApduResponse(hexResponse);\n this.logger.debug(formatApduReceivedLog(apduResponse));\n return Right(apduResponse);\n } catch (error) {\n if (this.connectedDevice) {\n this.logger.debug(\"disconnecting\");\n onDisconnect(deviceId);\n this.disconnect({\n connectedDevice: this.connectedDevice,\n });\n }\n return Left(new GeneralDmkError(error));\n }\n }\n\n private createApduResponse(hexApdu: string): ApduResponse {\n const apduResponse = {\n statusCode: this.fromHexString(\n hexApdu.substring(hexApdu.length - 4, hexApdu.length),\n ),\n data: this.fromHexString(hexApdu.substring(0, hexApdu.length - 4)),\n };\n return apduResponse;\n }\n\n //TODO: Move this to a helper\n private fromHexString(hexString: string): Uint8Array {\n if (!hexString) {\n return Uint8Array.from([]);\n }\n return new Uint8Array(\n hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),\n );\n }\n\n private listenForDisconnect(\n onDisconnect: DisconnectHandler,\n deviceId: DeviceId,\n ): void {\n this.disconnectInterval = setInterval(async () => {\n const isServerAvailable =\n await this._speculosDataSource.isServerAvailable();\n\n if (!isServerAvailable) {\n this.logger.info(\n `Speculos server unavailable, disconnecting device ${deviceId}`,\n );\n onDisconnect(deviceId);\n\n if (this.connectedDevice) {\n await this.disconnect({\n connectedDevice: this.connectedDevice,\n });\n }\n\n this.clearDisconnectInterval();\n }\n }, 2000);\n }\n\n private clearDisconnectInterval(): void {\n if (this.disconnectInterval) {\n clearInterval(this.disconnectInterval);\n this.disconnectInterval = null;\n }\n }\n}\n\nexport const speculosTransportFactory: (\n speculosUrl?: string,\n isE2E?: boolean,\n deviceModelId?: DeviceModelId,\n options?: SpeculosTransportOptions,\n) => TransportFactory =\n (\n speculosUrl = \"http://127.0.0.1:5000\",\n isE2E = false,\n deviceModelId?,\n options?,\n ) =>\n ({ config, loggerServiceFactory }) =>\n new SpeculosTransport(\n loggerServiceFactory,\n config,\n speculosUrl,\n isE2E,\n deviceModelId,\n options,\n );\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["SpeculosTransport_exports", "__export", "SpeculosTransport", "speculosIdentifier", "speculosTransportFactory", "__toCommonJS", "import_device_management_kit", "import_purify_ts", "import_rxjs", "import_HttpSpeculosDatasource", "loggerServiceFactory", "_config", "speculosUrl", "isE2E", "deviceModelId", "bearerToken", "params", "hexResponse", "apduResponse", "parser", "appName", "appVersion", "sessionId", "connectedDevice", "apdu", "error", "_params", "_sessionId", "deviceId", "onDisconnect", "hexApdu", "hexString", "byte", "options", "config"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var c=require("@ledgerhq/device-management-kit"),e=require("vitest"),t=require("./SpeculosTransport"),s=require("../internal/datasource/HttpSpeculosDatasource");const p="0108457468657265756d0a312e32332e302d64657601009000",i=e.vi.fn(),n=e.vi.fn();e.vi.mock("@internal/datasource/HttpSpeculosDatasource",()=>({HttpSpeculosDatasource:e.vi.fn(()=>({postApdu:i,isServerAvailable:n,openEventStream:e.vi.fn()}))}));const a=()=>({debug:e.vi.fn(),info:e.vi.fn(),warn:e.vi.fn(),error:e.vi.fn()});(0,e.describe)("SpeculosTransport",()=>{(0,e.beforeEach)(()=>{e.vi.clearAllMocks(),e.vi.useFakeTimers(),i.mockResolvedValue(p),n.mockResolvedValue(!0)}),(0,e.afterEach)(()=>{e.vi.useRealTimers()});const l=async()=>{const o=new t.SpeculosTransport(a,{},"http://localhost:5000"),r=await o.connect({deviceId:"SpeculosID",onDisconnect:e.vi.fn()});return{transport:o,connectedDevice:r.unsafeCoerce()}};(0,e.describe)("disconnect",()=>{(0,e.it)("should stop polling the server once disconnected",async()=>{const{transport:o,connectedDevice:r}=await l();await e.vi.advanceTimersByTimeAsync(2e3),(0,e.expect)(n).toHaveBeenCalledTimes(1),await o.disconnect({connectedDevice:r}),n.mockClear(),await e.vi.advanceTimersByTimeAsync(1e4),(0,e.expect)(n).not.toHaveBeenCalled()})}),(0,e.describe)("bearer token",()=>{const o="session-token";(0,e.it)("should pass the bearer token to the datasource",()=>{new t.SpeculosTransport(a,{},"http://localhost:5000",!0,c.DeviceModelId.NANO_X,{bearerToken:o}),(0,e.expect)(s.HttpSpeculosDatasource).toHaveBeenCalledWith("http://localhost:5000",void 0,o)}),(0,e.it)("should default to the local Speculos without a bearer token",()=>{(0,t.speculosTransportFactory)()({config:{},loggerServiceFactory:a}),(0,e.expect)(s.HttpSpeculosDatasource).toHaveBeenCalledWith("http://127.0.0.1:5000",void 0,void 0)}),(0,e.it)("should pass the bearer token through the factory",()=>{(0,t.speculosTransportFactory)("http://localhost:5001",!0,c.DeviceModelId.NANO_X,{bearerToken:o})({config:{},loggerServiceFactory:a}),(0,e.expect)(s.HttpSpeculosDatasource).toHaveBeenCalledWith("http://localhost:5001",void 0,o)})})});
|
|
2
|
+
//# sourceMappingURL=SpeculosTransport.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/api/SpeculosTransport.test.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n DeviceModelId,\n type DmkConfig,\n type LoggerPublisherService,\n type TransportArgs,\n type TransportConnectedDevice,\n} from \"@ledgerhq/device-management-kit\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport {\n SpeculosTransport,\n speculosTransportFactory,\n} from \"@api/SpeculosTransport\";\nimport { HttpSpeculosDatasource } from \"@internal/datasource/HttpSpeculosDatasource\";\n\nconst GET_APP_AND_VERSION_RESPONSE =\n \"0108457468657265756d0a312e32332e302d64657601009000\";\n\nconst postApdu = vi.fn();\nconst isServerAvailable = vi.fn();\n\nvi.mock(\"@internal/datasource/HttpSpeculosDatasource\", () => ({\n HttpSpeculosDatasource: vi.fn(() => ({\n postApdu,\n isServerAvailable,\n openEventStream: vi.fn(),\n })),\n}));\n\nconst loggerFactory = () =>\n ({\n debug: vi.fn(),\n info: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n }) as unknown as LoggerPublisherService;\n\ndescribe(\"SpeculosTransport\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n vi.useFakeTimers();\n postApdu.mockResolvedValue(GET_APP_AND_VERSION_RESPONSE);\n isServerAvailable.mockResolvedValue(true);\n });\n\n afterEach(() => {\n // Restore real timers so the fake ones cannot leak into another test file.\n vi.useRealTimers();\n });\n\n const connect = async () => {\n const transport = new SpeculosTransport(\n loggerFactory,\n {} as DmkConfig,\n \"http://localhost:5000\",\n );\n const result = await transport.connect({\n deviceId: \"SpeculosID\",\n onDisconnect: vi.fn(),\n });\n return {\n transport,\n connectedDevice: result.unsafeCoerce() as TransportConnectedDevice,\n };\n };\n\n describe(\"disconnect\", () => {\n it(\"should stop polling the server once disconnected\", async () => {\n const { transport, connectedDevice } = await connect();\n await vi.advanceTimersByTimeAsync(2000);\n expect(isServerAvailable).toHaveBeenCalledTimes(1);\n\n await transport.disconnect({ connectedDevice });\n isServerAvailable.mockClear();\n await vi.advanceTimersByTimeAsync(10_000);\n\n expect(isServerAvailable).not.toHaveBeenCalled();\n });\n });\n\n describe(\"bearer token\", () => {\n const bearerToken = \"session-token\";\n\n it(\"should pass the bearer token to the datasource\", () => {\n new SpeculosTransport(\n loggerFactory,\n {} as DmkConfig,\n \"http://localhost:5000\",\n true,\n DeviceModelId.NANO_X,\n { bearerToken },\n );\n\n expect(HttpSpeculosDatasource).toHaveBeenCalledWith(\n \"http://localhost:5000\",\n undefined,\n bearerToken,\n );\n });\n\n it(\"should default to the local Speculos without a bearer token\", () => {\n speculosTransportFactory()({\n config: {} as DmkConfig,\n loggerServiceFactory: loggerFactory,\n } as unknown as TransportArgs);\n\n expect(HttpSpeculosDatasource).toHaveBeenCalledWith(\n \"http://127.0.0.1:5000\",\n undefined,\n undefined,\n );\n });\n\n it(\"should pass the bearer token through the factory\", () => {\n speculosTransportFactory(\n \"http://localhost:5001\",\n true,\n DeviceModelId.NANO_X,\n { bearerToken },\n )({\n config: {} as DmkConfig,\n loggerServiceFactory: loggerFactory,\n } as unknown as TransportArgs);\n\n expect(HttpSpeculosDatasource).toHaveBeenCalledWith(\n \"http://localhost:5001\",\n undefined,\n bearerToken,\n );\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "aAAA,IAAAA,EAMO,2CACPC,EAAgE,kBAEhEC,EAGO,kCACPC,EAAuC,uDAEvC,MAAMC,EACJ,qDAEIC,EAAW,KAAG,GAAG,EACjBC,EAAoB,KAAG,GAAG,EAEhC,KAAG,KAAK,8CAA+C,KAAO,CAC5D,uBAAwB,KAAG,GAAG,KAAO,CACnC,SAAAD,EACA,kBAAAC,EACA,gBAAiB,KAAG,GAAG,CACzB,EAAE,CACJ,EAAE,EAEF,MAAMC,EAAgB,KACnB,CACC,MAAO,KAAG,GAAG,EACb,KAAM,KAAG,GAAG,EACZ,KAAM,KAAG,GAAG,EACZ,MAAO,KAAG,GAAG,CACf,MAEF,YAAS,oBAAqB,IAAM,IAClC,cAAW,IAAM,CACf,KAAG,cAAc,EACjB,KAAG,cAAc,EACjBF,EAAS,kBAAkBD,CAA4B,EACvDE,EAAkB,kBAAkB,EAAI,CAC1C,CAAC,KAED,aAAU,IAAM,CAEd,KAAG,cAAc,CACnB,CAAC,EAED,MAAME,EAAU,SAAY,CAC1B,MAAMC,EAAY,IAAI,oBACpBF,EACA,CAAC,EACD,uBACF,EACMG,EAAS,MAAMD,EAAU,QAAQ,CACrC,SAAU,aACV,aAAc,KAAG,GAAG,CACtB,CAAC,EACD,MAAO,CACL,UAAAA,EACA,gBAAiBC,EAAO,aAAa,CACvC,CACF,KAEA,YAAS,aAAc,IAAM,IAC3B,MAAG,mDAAoD,SAAY,CACjE,KAAM,CAAE,UAAAD,EAAW,gBAAAE,CAAgB,EAAI,MAAMH,EAAQ,EACrD,MAAM,KAAG,yBAAyB,GAAI,KACtC,UAAOF,CAAiB,EAAE,sBAAsB,CAAC,EAEjD,MAAMG,EAAU,WAAW,CAAE,gBAAAE,CAAgB,CAAC,EAC9CL,EAAkB,UAAU,EAC5B,MAAM,KAAG,yBAAyB,GAAM,KAExC,UAAOA,CAAiB,EAAE,IAAI,iBAAiB,CACjD,CAAC,CACH,CAAC,KAED,YAAS,eAAgB,IAAM,CAC7B,MAAMM,EAAc,mBAEpB,MAAG,iDAAkD,IAAM,CACzD,IAAI,oBACFL,EACA,CAAC,EACD,wBACA,GACA,gBAAc,OACd,CAAE,YAAAK,CAAY,CAChB,KAEA,UAAO,wBAAsB,EAAE,qBAC7B,wBACA,OACAA,CACF,CACF,CAAC,KAED,MAAG,8DAA+D,IAAM,IACtE,4BAAyB,EAAE,CACzB,OAAQ,CAAC,EACT,qBAAsBL,CACxB,CAA6B,KAE7B,UAAO,wBAAsB,EAAE,qBAC7B,wBACA,OACA,MACF,CACF,CAAC,KAED,MAAG,mDAAoD,IAAM,IAC3D,4BACE,wBACA,GACA,gBAAc,OACd,CAAE,YAAAK,CAAY,CAChB,EAAE,CACA,OAAQ,CAAC,EACT,qBAAsBL,CACxB,CAA6B,KAE7B,UAAO,wBAAsB,EAAE,qBAC7B,wBACA,OACAK,CACF,CACF,CAAC,CACH,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["import_device_management_kit", "import_vitest", "import_SpeculosTransport", "import_HttpSpeculosDatasource", "GET_APP_AND_VERSION_RESPONSE", "postApdu", "isServerAvailable", "loggerFactory", "connect", "transport", "result", "connectedDevice", "bearerToken"]
|
|
7
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var A=Object.create;var c=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var k=Object.getPrototypeOf,C=Object.prototype.hasOwnProperty;var T=(e,t)=>{for(var r in t)c(e,r,{get:t[r],enumerable:!0})},f=(e,t,r,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of D(t))!C.call(e,s)&&s!==r&&c(e,s,{get:()=>t[s],enumerable:!(a=E(t,s))||a.enumerable});return e};var U=(e,t,r)=>(r=e!=null?A(k(e)):{},f(t||!e||!e.__esModule?c(r,"default",{value:e,enumerable:!0}):r,e)),$=e=>f(c({},"__esModule",{value:!0}),e);var R={};T(R,{HttpSpeculosDatasource:()=>O});module.exports=$(R);var m=require("@ledgerhq/device-management-kit"),g=U(require("../../../package.json"));const P=2e3,N=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t--;return e.slice(0,t)};class O{baseUrl;headers;http;constructor(t,r=`ldmk-transport-speculos/${g.default.version}`,a){this.baseUrl=N(t),this.headers={"X-Ledger-Client-Version":r,...a?{Authorization:`Bearer ${a}`}:{}},this.http=new m.DmkNetworkClient({headers:this.headers})}async postApdu(t){return(await this.http.post(`${this.baseUrl}/apdu`,{data:t})).data}async isServerAvailable(){try{return await this.http.get(`${this.baseUrl}/events`,{timeoutMs:P,responseType:"void"}),!0}catch{return!1}}async openEventStream(t,r){if(typeof fetch>"u")throw new Error("global fetch is not available in Node < 18");const a=`${this.baseUrl}/events?stream=true`,s=new AbortController,y={...this.headers,Accept:"text/event-stream","Cache-Control":"no-cache"},l=await fetch(a,{method:"GET",headers:y,signal:s.signal});if(!l.ok)throw s.abort(),new Error(`SSE request failed with status ${l.status}`);const d=l.body;if(!d)throw s.abort(),new Error("SSE response has no body stream.");const w=d.getReader(),b=new TextDecoder("utf-8");let o="",h=!1;const v=()=>{if(!h){h=!0;try{r?.()}catch{}}},p=n=>{if(n.startsWith("data: ")){const i=n.slice(6);try{t(JSON.parse(i))}catch{t({data:i})}}};return(async()=>{try{for(;;){const{value:n,done:i}=await w.read();if(i)break;o+=b.decode(n,{stream:!0});const u=o.split(/\r?\n/);o=u.pop()??"";for(const S of u)p(S)}}catch{}finally{if(o.length){for(const n of o.split(/\r?\n/))p(n);o=""}v()}})(),d}}0&&(module.exports={HttpSpeculosDatasource});
|
|
2
2
|
//# sourceMappingURL=HttpSpeculosDatasource.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/internal/datasource/HttpSpeculosDatasource.ts"],
|
|
4
|
-
"sourcesContent": ["import
|
|
5
|
-
"mappings": "0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,4BAAAE,
|
|
6
|
-
"names": ["HttpSpeculosDatasource_exports", "__export", "HttpSpeculosDatasource", "
|
|
4
|
+
"sourcesContent": ["import { DmkNetworkClient } from \"@ledgerhq/device-management-kit\";\n\nimport PACKAGE from \"@root/package.json\";\n\nimport { type SpeculosDatasource } from \"./SpeculosDatasource\";\n\nconst TIMEOUT = 2000; // 2 second timeout for availability check\n\n// A loop rather than /\\/+$/, which backtracks quadratically on long runs of slashes.\nconst removeTrailingSlashes = (url: string) => {\n let end = url.length;\n while (end > 0 && url[end - 1] === \"/\") end--;\n return url.slice(0, end);\n};\n\nexport class HttpSpeculosDatasource implements SpeculosDatasource {\n private readonly baseUrl: string;\n private readonly headers: Record<string, string>;\n private readonly http: DmkNetworkClient;\n\n constructor(\n baseUrl: string,\n clientHeader: string = `ldmk-transport-speculos/${PACKAGE.version}`,\n bearerToken?: string,\n ) {\n this.baseUrl = removeTrailingSlashes(baseUrl);\n this.headers = {\n \"X-Ledger-Client-Version\": clientHeader,\n ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),\n };\n this.http = new DmkNetworkClient({ headers: this.headers });\n }\n\n async postApdu(apdu: string): Promise<string> {\n const data = (await this.http.post(`${this.baseUrl}/apdu`, {\n data: apdu,\n })) as SpeculosApduDTO;\n return data.data;\n }\n\n async isServerAvailable(): Promise<boolean> {\n try {\n await this.http.get(`${this.baseUrl}/events`, {\n timeoutMs: TIMEOUT,\n responseType: \"void\",\n });\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * open an SSE event stream using the fetch API.\n * - calls `onEvent` for each line starting with \"data: \"\n * - calls `onClose` when the stream ends or errors\n * - returns the ReadableStream so callers can `cancel()` it\n */\n async openEventStream(\n onEvent: (json: Record<string, unknown>) => void,\n onClose?: () => void,\n ): Promise<ReadableStream<Uint8Array>> {\n if (typeof fetch === \"undefined\") {\n throw new Error(\"global fetch is not available in Node < 18\");\n }\n\n const url = `${this.baseUrl}/events?stream=true`;\n\n const controller = new AbortController();\n\n const headers: HeadersInit = {\n ...this.headers,\n Accept: \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n };\n\n const response = await fetch(url, {\n method: \"GET\",\n headers,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n controller.abort();\n throw new Error(`SSE request failed with status ${response.status}`);\n }\n\n const stream = response.body;\n if (!stream) {\n controller.abort();\n throw new Error(\"SSE response has no body stream.\");\n }\n\n const reader = stream.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let buffer = \"\";\n let closed = false;\n\n const finalize = () => {\n if (!closed) {\n closed = true;\n try {\n onClose?.();\n } catch {\n // swallow listener errors\n }\n }\n };\n\n const emitParsedEvent = (line: string) => {\n if (line.startsWith(\"data: \")) {\n const payload = line.slice(6);\n try {\n onEvent(JSON.parse(payload));\n } catch {\n onEvent({ data: payload });\n }\n }\n };\n\n (async () => {\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // normalise line breaks and process complete lines\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() ?? \"\"; // keep last partial line\n\n for (const line of lines) {\n emitParsedEvent(line);\n // other SSE fields ignored to mirror original behavior\n }\n }\n } catch {\n // network/reader error\n } finally {\n // flush any remaining buffered text as lines\n if (buffer.length) {\n for (const line of buffer.split(/\\r?\\n/)) {\n emitParsedEvent(line);\n }\n buffer = \"\";\n }\n finalize();\n }\n })();\n\n // consumers can cancel with: (await openEventStream(...)).cancel()\n return stream;\n }\n}\n\ntype SpeculosApduDTO = {\n data: string;\n};\n"],
|
|
5
|
+
"mappings": "0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,4BAAAE,IAAA,eAAAC,EAAAH,GAAA,IAAAI,EAAiC,2CAEjCC,EAAoB,iCAIpB,MAAMC,EAAU,IAGVC,EAAyBC,GAAgB,CAC7C,IAAIC,EAAMD,EAAI,OACd,KAAOC,EAAM,GAAKD,EAAIC,EAAM,CAAC,IAAM,KAAKA,IACxC,OAAOD,EAAI,MAAM,EAAGC,CAAG,CACzB,EAEO,MAAMP,CAAqD,CAC/C,QACA,QACA,KAEjB,YACEQ,EACAC,EAAuB,2BAA2B,EAAAC,QAAQ,OAAO,GACjEC,EACA,CACA,KAAK,QAAUN,EAAsBG,CAAO,EAC5C,KAAK,QAAU,CACb,0BAA2BC,EAC3B,GAAIE,EAAc,CAAE,cAAe,UAAUA,CAAW,EAAG,EAAI,CAAC,CAClE,EACA,KAAK,KAAO,IAAI,mBAAiB,CAAE,QAAS,KAAK,OAAQ,CAAC,CAC5D,CAEA,MAAM,SAASC,EAA+B,CAI5C,OAHc,MAAM,KAAK,KAAK,KAAK,GAAG,KAAK,OAAO,QAAS,CACzD,KAAMA,CACR,CAAC,GACW,IACd,CAEA,MAAM,mBAAsC,CAC1C,GAAI,CACF,aAAM,KAAK,KAAK,IAAI,GAAG,KAAK,OAAO,UAAW,CAC5C,UAAWR,EACX,aAAc,MAChB,CAAC,EACM,EACT,MAAQ,CACN,MAAO,EACT,CACF,CAQA,MAAM,gBACJS,EACAC,EACqC,CACrC,GAAI,OAAO,MAAU,IACnB,MAAM,IAAI,MAAM,4CAA4C,EAG9D,MAAMR,EAAM,GAAG,KAAK,OAAO,sBAErBS,EAAa,IAAI,gBAEjBC,EAAuB,CAC3B,GAAG,KAAK,QACR,OAAQ,oBACR,gBAAiB,UACnB,EAEMC,EAAW,MAAM,MAAMX,EAAK,CAChC,OAAQ,MACR,QAAAU,EACA,OAAQD,EAAW,MACrB,CAAC,EAED,GAAI,CAACE,EAAS,GACZ,MAAAF,EAAW,MAAM,EACX,IAAI,MAAM,kCAAkCE,EAAS,MAAM,EAAE,EAGrE,MAAMC,EAASD,EAAS,KACxB,GAAI,CAACC,EACH,MAAAH,EAAW,MAAM,EACX,IAAI,MAAM,kCAAkC,EAGpD,MAAMI,EAASD,EAAO,UAAU,EAC1BE,EAAU,IAAI,YAAY,OAAO,EACvC,IAAIC,EAAS,GACTC,EAAS,GAEb,MAAMC,EAAW,IAAM,CACrB,GAAI,CAACD,EAAQ,CACXA,EAAS,GACT,GAAI,CACFR,IAAU,CACZ,MAAQ,CAER,CACF,CACF,EAEMU,EAAmBC,GAAiB,CACxC,GAAIA,EAAK,WAAW,QAAQ,EAAG,CAC7B,MAAMC,EAAUD,EAAK,MAAM,CAAC,EAC5B,GAAI,CACFZ,EAAQ,KAAK,MAAMa,CAAO,CAAC,CAC7B,MAAQ,CACNb,EAAQ,CAAE,KAAMa,CAAQ,CAAC,CAC3B,CACF,CACF,EAEA,OAAC,SAAY,CACX,GAAI,CACF,OAAa,CACX,KAAM,CAAE,MAAAC,EAAO,KAAAC,CAAK,EAAI,MAAMT,EAAO,KAAK,EAC1C,GAAIS,EAAM,MACVP,GAAUD,EAAQ,OAAOO,EAAO,CAAE,OAAQ,EAAK,CAAC,EAGhD,MAAME,EAAQR,EAAO,MAAM,OAAO,EAClCA,EAASQ,EAAM,IAAI,GAAK,GAExB,UAAWJ,KAAQI,EACjBL,EAAgBC,CAAI,CAGxB,CACF,MAAQ,CAER,QAAE,CAEA,GAAIJ,EAAO,OAAQ,CACjB,UAAWI,KAAQJ,EAAO,MAAM,OAAO,EACrCG,EAAgBC,CAAI,EAEtBJ,EAAS,EACX,CACAE,EAAS,CACX,CACF,GAAG,EAGIL,CACT,CACF",
|
|
6
|
+
"names": ["HttpSpeculosDatasource_exports", "__export", "HttpSpeculosDatasource", "__toCommonJS", "import_device_management_kit", "import_package", "TIMEOUT", "removeTrailingSlashes", "url", "end", "baseUrl", "clientHeader", "PACKAGE", "bearerToken", "apdu", "onEvent", "onClose", "controller", "headers", "response", "stream", "reader", "decoder", "buffer", "closed", "finalize", "emitParsedEvent", "line", "payload", "value", "done", "lines"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";var e=require("vitest"),o=require("./HttpSpeculosDatasource");const s="session-token",a={Authorization:`Bearer ${s}`},c=t=>new Response(JSON.stringify(t),{headers:{"Content-Type":"application/json"}});(0,e.afterEach)(()=>{e.vi.restoreAllMocks()});(0,e.describe)("HttpSpeculosDatasource",()=>{(0,e.it)("sends only the client header by default",async()=>{const t=e.vi.spyOn(globalThis,"fetch").mockResolvedValue(c({data:"9000"}));await new o.HttpSpeculosDatasource("http://speculos/","client").postApdu("b001000000"),(0,e.expect)(t).toHaveBeenCalledWith("http://speculos/apdu",e.expect.objectContaining({method:"POST",headers:e.expect.objectContaining({"X-Ledger-Client-Version":"client"})}));const[,n]=t.mock.calls[0];(0,e.expect)(n?.headers).not.toHaveProperty("Authorization")}),(0,e.it)("strips every trailing slash from the base URL",async()=>{const t=e.vi.spyOn(globalThis,"fetch").mockImplementation(()=>Promise.resolve(c({data:"9000"})));await new o.HttpSpeculosDatasource("http://speculos///").postApdu("b0"),await new o.HttpSpeculosDatasource("///").postApdu("b0"),(0,e.expect)(t.mock.calls.map(([n])=>n)).toEqual(["http://speculos/apdu","/apdu"])}),(0,e.it)("sends the bearer token with APDUs",async()=>{const t=e.vi.spyOn(globalThis,"fetch").mockResolvedValue(c({data:"9000"})),n=await new o.HttpSpeculosDatasource("http://speculos","client",s).postApdu("b001000000");(0,e.expect)(n).toBe("9000"),(0,e.expect)(t).toHaveBeenCalledWith("http://speculos/apdu",e.expect.objectContaining({headers:e.expect.objectContaining({...a,"X-Ledger-Client-Version":"client"})}))}),(0,e.it)("sends the bearer token with the availability check",async()=>{const t=e.vi.spyOn(globalThis,"fetch").mockResolvedValue(new Response(null)),n=await new o.HttpSpeculosDatasource("http://speculos","client",s).isServerAvailable();(0,e.expect)(n).toBe(!0),(0,e.expect)(t).toHaveBeenCalledWith("http://speculos/events",e.expect.objectContaining({method:"GET",headers:e.expect.objectContaining(a)}))}),(0,e.it)("sends the bearer token with the event stream",async()=>{const t=e.vi.spyOn(globalThis,"fetch").mockResolvedValue(new Response(`data: {"text":"Ready"}
|
|
2
|
+
|
|
3
|
+
`,{headers:{"Content-Type":"text/event-stream"}})),n=e.vi.fn();await new Promise((i,l)=>{new o.HttpSpeculosDatasource("http://speculos","client",s).openEventStream(n,i).catch(l)}),(0,e.expect)(t).toHaveBeenCalledWith("http://speculos/events?stream=true",e.expect.objectContaining({headers:{...a,"X-Ledger-Client-Version":"client",Accept:"text/event-stream","Cache-Control":"no-cache"}})),(0,e.expect)(n).toHaveBeenCalledWith({text:"Ready"})})});
|
|
4
|
+
//# sourceMappingURL=HttpSpeculosDatasource.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/internal/datasource/HttpSpeculosDatasource.test.ts"],
|
|
4
|
+
"sourcesContent": ["import { afterEach, describe, expect, it, vi } from \"vitest\";\n\nimport { HttpSpeculosDatasource } from \"./HttpSpeculosDatasource\";\n\nconst TOKEN = \"session-token\";\nconst AUTHORIZATION = { Authorization: `Bearer ${TOKEN}` };\n\nconst jsonResponse = (body: unknown) =>\n new Response(JSON.stringify(body), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n\nafterEach(() => {\n vi.restoreAllMocks();\n});\n\ndescribe(\"HttpSpeculosDatasource\", () => {\n it(\"sends only the client header by default\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockResolvedValue(jsonResponse({ data: \"9000\" }));\n\n await new HttpSpeculosDatasource(\"http://speculos/\", \"client\").postApdu(\n \"b001000000\",\n );\n\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/apdu\",\n expect.objectContaining({\n method: \"POST\",\n headers: expect.objectContaining({\n \"X-Ledger-Client-Version\": \"client\",\n }),\n }),\n );\n const [, init] = fetchSpy.mock.calls[0]!;\n expect(init?.headers).not.toHaveProperty(\"Authorization\");\n });\n\n it(\"strips every trailing slash from the base URL\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockImplementation(() =>\n Promise.resolve(jsonResponse({ data: \"9000\" })),\n );\n\n await new HttpSpeculosDatasource(\"http://speculos///\").postApdu(\"b0\");\n await new HttpSpeculosDatasource(\"///\").postApdu(\"b0\");\n\n expect(fetchSpy.mock.calls.map(([url]) => url)).toEqual([\n \"http://speculos/apdu\",\n \"/apdu\",\n ]);\n });\n\n it(\"sends the bearer token with APDUs\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockResolvedValue(jsonResponse({ data: \"9000\" }));\n\n const response = await new HttpSpeculosDatasource(\n \"http://speculos\",\n \"client\",\n TOKEN,\n ).postApdu(\"b001000000\");\n\n expect(response).toBe(\"9000\");\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/apdu\",\n expect.objectContaining({\n headers: expect.objectContaining({\n ...AUTHORIZATION,\n \"X-Ledger-Client-Version\": \"client\",\n }),\n }),\n );\n });\n\n it(\"sends the bearer token with the availability check\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockResolvedValue(new Response(null));\n\n const available = await new HttpSpeculosDatasource(\n \"http://speculos\",\n \"client\",\n TOKEN,\n ).isServerAvailable();\n\n expect(available).toBe(true);\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/events\",\n expect.objectContaining({\n method: \"GET\",\n headers: expect.objectContaining(AUTHORIZATION),\n }),\n );\n });\n\n it(\"sends the bearer token with the event stream\", async () => {\n const fetchSpy = vi.spyOn(globalThis, \"fetch\").mockResolvedValue(\n new Response('data: {\"text\":\"Ready\"}\\n\\n', {\n headers: { \"Content-Type\": \"text/event-stream\" },\n }),\n );\n const onEvent = vi.fn();\n\n await new Promise<void>((resolve, reject) => {\n new HttpSpeculosDatasource(\"http://speculos\", \"client\", TOKEN)\n .openEventStream(onEvent, resolve)\n .catch(reject);\n });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/events?stream=true\",\n expect.objectContaining({\n headers: {\n ...AUTHORIZATION,\n \"X-Ledger-Client-Version\": \"client\",\n Accept: \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n },\n }),\n );\n expect(onEvent).toHaveBeenCalledWith({ text: \"Ready\" });\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_vitest", "import_HttpSpeculosDatasource", "TOKEN", "AUTHORIZATION", "jsonResponse", "body", "fetchSpy", "init", "url", "response", "available", "onEvent", "resolve", "reject"]
|
|
7
|
+
}
|
package/lib/esm/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@sentry/minimal": "catalog:",
|
|
4
|
-
"axios": "catalog:",
|
|
5
4
|
"purify-ts": "catalog:"
|
|
6
5
|
},
|
|
7
6
|
"devDependencies": {
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
"license": "Apache-2.0",
|
|
33
32
|
"name": "@ledgerhq/device-transport-kit-speculos",
|
|
34
33
|
"peerDependencies": {
|
|
35
|
-
"@ledgerhq/device-management-kit": "
|
|
34
|
+
"@ledgerhq/device-management-kit": "^1.11.0",
|
|
36
35
|
"rxjs": "catalog:"
|
|
37
36
|
},
|
|
38
37
|
"private": false,
|
|
@@ -56,5 +55,5 @@
|
|
|
56
55
|
"watch:builds": "pnpm ldmk-tool watch --entryPoints index.ts,src/**/*.ts --tsconfig tsconfig.prod.json",
|
|
57
56
|
"watch:types": "concurrently \"tsc --watch -p tsconfig.prod.json\" \"tsc-alias --watch -p tsconfig.prod.json\""
|
|
58
57
|
},
|
|
59
|
-
"version": "1.
|
|
58
|
+
"version": "1.3.0"
|
|
60
59
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ApduParser as v,bufferToHexaString as g,DeviceModelId as D,formatApduReceivedLog as h,formatApduSentLog as
|
|
1
|
+
import{ApduParser as v,bufferToHexaString as g,DeviceModelId as D,formatApduReceivedLog as h,formatApduSentLog as S,GeneralDmkError as f,OpeningConnectionError as m}from"@ledgerhq/device-management-kit";import{Left as d,Right as a}from"purify-ts";import{from as l}from"rxjs";import{HttpSpeculosDatasource as b}from"../internal/datasource/HttpSpeculosDatasource";const y="SPECULOS_HTTP_TRANSPORT";class I{logger;identifier=y;_speculosDataSource;connectedDevice=null;disconnectInterval=null;_isE2E;speculosDevice;constructor(e,t,o,r,n=D.STAX,{bearerToken:s}={}){this._isE2E=r??!1,this.logger=e("SpeculosTransport"),this._speculosDataSource=new b(o,void 0,s),this.speculosDevice={id:"SpeculosID",deviceModel:{id:n,productName:"Speculos - App Name - version",usbProductId:16,bootloaderUsbProductId:1,getBlockSize(){return 32},usbOnly:!0,memorySize:320*1024,masks:[823132160]},transport:this.identifier}}isSupported(){return!0}getIdentifier(){return this.identifier}listenToAvailableDevices(){return l([[this.speculosDevice]])}startDiscovering(){return this.logger.debug("startDiscovering"),l([this.speculosDevice])}stopDiscovering(){this.logger.debug("stopDiscovering")}async connect(e){this.logger.debug("connect");const t=await this._speculosDataSource.postApdu("B0010000");this.logger.debug(`Hex Response: ${t}`);const o=this.createApduResponse(t),r=new v(o);r.extract8BitUInt();const n=r.encodeToString(r.extractFieldLVEncoded()),s=r.encodeToString(r.extractFieldLVEncoded());this.logger.debug(`App Name: ${n} and version ${s}`);const i=e.deviceId;try{const c={sendApdu:u=>this.sendApdu(i,e.deviceId,e.onDisconnect,u),deviceModel:{...this.speculosDevice.deviceModel,productName:`Speculos - ${n} - ${s}`,getBlockSize(){return 32}},transport:this.identifier,id:"SpeculosID",type:"USB"};return this.connectedDevice=c,this._isE2E||this.listenForDisconnect(e.onDisconnect,e.deviceId),a(c)}catch(c){return d(new m(c))}}async disconnect(e){return this.logger.debug("disconnect"),this.connectedDevice=null,this.clearDisconnectInterval(),Promise.resolve(a(void 0))}async sendApdu(e,t,o,r){try{const n=g(r).substring(2),s=await this._speculosDataSource.postApdu(n);this.logger.debug(S(r));const i=this.createApduResponse(s);return this.logger.debug(h(i)),a(i)}catch(n){return this.connectedDevice&&(this.logger.debug("disconnecting"),o(t),this.disconnect({connectedDevice:this.connectedDevice})),d(new f(n))}}createApduResponse(e){return{statusCode:this.fromHexString(e.substring(e.length-4,e.length)),data:this.fromHexString(e.substring(0,e.length-4))}}fromHexString(e){return e?new Uint8Array(e.match(/.{1,2}/g).map(t=>parseInt(t,16))):Uint8Array.from([])}listenForDisconnect(e,t){this.disconnectInterval=setInterval(async()=>{await this._speculosDataSource.isServerAvailable()||(this.logger.info(`Speculos server unavailable, disconnecting device ${t}`),e(t),this.connectedDevice&&await this.disconnect({connectedDevice:this.connectedDevice}),this.clearDisconnectInterval())},2e3)}clearDisconnectInterval(){this.disconnectInterval&&(clearInterval(this.disconnectInterval),this.disconnectInterval=null)}}const x=(p="http://127.0.0.1:5000",e=!1,t,o)=>({config:r,loggerServiceFactory:n})=>new I(n,r,p,e,t,o);export{I as SpeculosTransport,y as speculosIdentifier,x as speculosTransportFactory};
|
|
2
2
|
//# sourceMappingURL=SpeculosTransport.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/api/SpeculosTransport.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n ApduParser,\n type ApduResponse,\n bufferToHexaString,\n type ConnectError,\n type DeviceId,\n DeviceModelId,\n type DisconnectHandler,\n type DmkConfig,\n type DmkError,\n formatApduReceivedLog,\n formatApduSentLog,\n GeneralDmkError,\n type LoggerPublisherService,\n OpeningConnectionError,\n type Transport,\n type TransportConnectedDevice,\n type TransportDiscoveredDevice,\n type TransportFactory,\n type TransportIdentifier,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Right } from \"purify-ts\";\nimport { from, type Observable } from \"rxjs\";\n\nimport { HttpSpeculosDatasource } from \"@internal/datasource/HttpSpeculosDatasource\";\nimport { type SpeculosDatasource } from \"@internal/datasource/SpeculosDatasource\";\n\nexport const speculosIdentifier: TransportIdentifier =\n \"SPECULOS_HTTP_TRANSPORT\";\n\nexport class SpeculosTransport implements Transport {\n private logger: LoggerPublisherService;\n private readonly identifier: TransportIdentifier = speculosIdentifier;\n private readonly _speculosDataSource: SpeculosDatasource;\n private connectedDevice: TransportConnectedDevice | null = null;\n private disconnectInterval: NodeJS.Timeout | null = null;\n private readonly _isE2E: boolean;\n private readonly speculosDevice: TransportDiscoveredDevice;\n\n constructor(\n loggerServiceFactory: (tag: string) => LoggerPublisherService,\n _config: DmkConfig,\n speculosUrl: string,\n isE2E?: boolean,\n deviceModelId: DeviceModelId = DeviceModelId.STAX,\n ) {\n this._isE2E = isE2E ?? false;\n this.logger = loggerServiceFactory(\"SpeculosTransport\");\n this._speculosDataSource = new HttpSpeculosDatasource(speculosUrl);\n this.speculosDevice = {\n id: \"SpeculosID\",\n deviceModel: {\n id: deviceModelId,\n productName: \"Speculos - App Name - version\",\n usbProductId: 0x10,\n bootloaderUsbProductId: 0x0001,\n getBlockSize() {\n return 32;\n },\n usbOnly: true,\n memorySize: 320 * 1024,\n masks: [0x31100000],\n },\n transport: this.identifier,\n };\n }\n\n isSupported(): boolean {\n return true;\n }\n\n getIdentifier(): TransportIdentifier {\n return this.identifier;\n }\n\n listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]> {\n return from([[this.speculosDevice]]);\n }\n\n startDiscovering(): Observable<TransportDiscoveredDevice> {\n this.logger.debug(\"startDiscovering\");\n return from([this.speculosDevice]);\n }\n\n stopDiscovering(): void {\n //DO NOTHING HERE\n this.logger.debug(\"stopDiscovering\");\n }\n\n async connect(params: {\n deviceId: DeviceId;\n onDisconnect: DisconnectHandler;\n }): Promise<Either<ConnectError, TransportConnectedDevice>> {\n this.logger.debug(\"connect\");\n\n const hexResponse = await this._speculosDataSource.postApdu(\"B0010000\");\n this.logger.debug(`Hex Response: ${hexResponse}`);\n const apduResponse = this.createApduResponse(hexResponse);\n const parser = new ApduParser(apduResponse);\n\n //Copy paste from GetAppAndVersionCommand\n parser.extract8BitUInt(); //Need otherwise parser is not in the right position\n const appName = parser.encodeToString(parser.extractFieldLVEncoded());\n const appVersion = parser.encodeToString(parser.extractFieldLVEncoded());\n\n this.logger.debug(`App Name: ${appName} and version ${appVersion}`);\n\n const sessionId: string = params.deviceId;\n try {\n const connectedDevice: TransportConnectedDevice = {\n sendApdu: (apdu) => {\n return this.sendApdu(\n sessionId,\n params.deviceId,\n params.onDisconnect,\n apdu,\n );\n },\n deviceModel: {\n ...this.speculosDevice.deviceModel,\n productName: `Speculos - ${appName} - ${appVersion}`,\n getBlockSize() {\n return 32;\n },\n },\n transport: this.identifier,\n id: \"SpeculosID\", //TODO make it dynamic at creation\n type: \"USB\",\n };\n\n this.connectedDevice = connectedDevice;\n if (!this._isE2E) {\n this.listenForDisconnect(params.onDisconnect, params.deviceId);\n }\n return Right(connectedDevice);\n } catch (error) {\n return Left(new OpeningConnectionError(error as Error));\n }\n }\n\n async disconnect(_params: {\n connectedDevice: TransportConnectedDevice;\n }): Promise<Either<DmkError, void>> {\n this.logger.debug(\"disconnect\");\n this.connectedDevice = null;\n return Promise.resolve(Right(undefined));\n }\n\n async sendApdu(\n _sessionId: string,\n deviceId: DeviceId,\n onDisconnect: DisconnectHandler,\n apdu: Uint8Array,\n ): Promise<Either<DmkError, ApduResponse>> {\n try {\n const hexApdu = bufferToHexaString(apdu).substring(2);\n const hexResponse: string =\n await this._speculosDataSource.postApdu(hexApdu);\n this.logger.debug(formatApduSentLog(apdu));\n const apduResponse = this.createApduResponse(hexResponse);\n this.logger.debug(formatApduReceivedLog(apduResponse));\n return Right(apduResponse);\n } catch (error) {\n if (this.connectedDevice) {\n this.logger.debug(\"disconnecting\");\n onDisconnect(deviceId);\n this.disconnect({\n connectedDevice: this.connectedDevice,\n });\n
|
|
5
|
-
"mappings": "AAAA,OACE,cAAAA,EAEA,sBAAAC,EAGA,iBAAAC,EAIA,yBAAAC,EACA,qBAAAC,EACA,mBAAAC,EAEA,0BAAAC,MAMK,kCACP,OAAsB,QAAAC,EAAM,SAAAC,MAAa,YACzC,OAAS,QAAAC,MAA6B,OAEtC,OAAS,0BAAAC,MAA8B,8CAGhC,MAAMC,EACX,
|
|
6
|
-
"names": ["ApduParser", "bufferToHexaString", "DeviceModelId", "formatApduReceivedLog", "formatApduSentLog", "GeneralDmkError", "OpeningConnectionError", "Left", "Right", "from", "HttpSpeculosDatasource", "speculosIdentifier", "SpeculosTransport", "loggerServiceFactory", "_config", "speculosUrl", "isE2E", "deviceModelId", "params", "hexResponse", "apduResponse", "parser", "appName", "appVersion", "sessionId", "connectedDevice", "apdu", "error", "_params", "_sessionId", "deviceId", "onDisconnect", "hexApdu", "hexString", "byte", "speculosTransportFactory", "config"]
|
|
4
|
+
"sourcesContent": ["import {\n ApduParser,\n type ApduResponse,\n bufferToHexaString,\n type ConnectError,\n type DeviceId,\n DeviceModelId,\n type DisconnectHandler,\n type DmkConfig,\n type DmkError,\n formatApduReceivedLog,\n formatApduSentLog,\n GeneralDmkError,\n type LoggerPublisherService,\n OpeningConnectionError,\n type Transport,\n type TransportConnectedDevice,\n type TransportDiscoveredDevice,\n type TransportFactory,\n type TransportIdentifier,\n} from \"@ledgerhq/device-management-kit\";\nimport { type Either, Left, Right } from \"purify-ts\";\nimport { from, type Observable } from \"rxjs\";\n\nimport { HttpSpeculosDatasource } from \"@internal/datasource/HttpSpeculosDatasource\";\nimport { type SpeculosDatasource } from \"@internal/datasource/SpeculosDatasource\";\n\nexport const speculosIdentifier: TransportIdentifier =\n \"SPECULOS_HTTP_TRANSPORT\";\n\nexport type SpeculosTransportOptions = {\n bearerToken?: string;\n};\n\nexport class SpeculosTransport implements Transport {\n private logger: LoggerPublisherService;\n private readonly identifier: TransportIdentifier = speculosIdentifier;\n private readonly _speculosDataSource: SpeculosDatasource;\n private connectedDevice: TransportConnectedDevice | null = null;\n private disconnectInterval: NodeJS.Timeout | null = null;\n private readonly _isE2E: boolean;\n private readonly speculosDevice: TransportDiscoveredDevice;\n\n constructor(\n loggerServiceFactory: (tag: string) => LoggerPublisherService,\n _config: DmkConfig,\n speculosUrl: string,\n isE2E?: boolean,\n deviceModelId: DeviceModelId = DeviceModelId.STAX,\n { bearerToken }: SpeculosTransportOptions = {},\n ) {\n this._isE2E = isE2E ?? false;\n this.logger = loggerServiceFactory(\"SpeculosTransport\");\n this._speculosDataSource = new HttpSpeculosDatasource(\n speculosUrl,\n undefined,\n bearerToken,\n );\n this.speculosDevice = {\n id: \"SpeculosID\",\n deviceModel: {\n id: deviceModelId,\n productName: \"Speculos - App Name - version\",\n usbProductId: 0x10,\n bootloaderUsbProductId: 0x0001,\n getBlockSize() {\n return 32;\n },\n usbOnly: true,\n memorySize: 320 * 1024,\n masks: [0x31100000],\n },\n transport: this.identifier,\n };\n }\n\n isSupported(): boolean {\n return true;\n }\n\n getIdentifier(): TransportIdentifier {\n return this.identifier;\n }\n\n listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]> {\n return from([[this.speculosDevice]]);\n }\n\n startDiscovering(): Observable<TransportDiscoveredDevice> {\n this.logger.debug(\"startDiscovering\");\n return from([this.speculosDevice]);\n }\n\n stopDiscovering(): void {\n //DO NOTHING HERE\n this.logger.debug(\"stopDiscovering\");\n }\n\n async connect(params: {\n deviceId: DeviceId;\n onDisconnect: DisconnectHandler;\n }): Promise<Either<ConnectError, TransportConnectedDevice>> {\n this.logger.debug(\"connect\");\n\n const hexResponse = await this._speculosDataSource.postApdu(\"B0010000\");\n this.logger.debug(`Hex Response: ${hexResponse}`);\n const apduResponse = this.createApduResponse(hexResponse);\n const parser = new ApduParser(apduResponse);\n\n //Copy paste from GetAppAndVersionCommand\n parser.extract8BitUInt(); //Need otherwise parser is not in the right position\n const appName = parser.encodeToString(parser.extractFieldLVEncoded());\n const appVersion = parser.encodeToString(parser.extractFieldLVEncoded());\n\n this.logger.debug(`App Name: ${appName} and version ${appVersion}`);\n\n const sessionId: string = params.deviceId;\n try {\n const connectedDevice: TransportConnectedDevice = {\n sendApdu: (apdu) => {\n return this.sendApdu(\n sessionId,\n params.deviceId,\n params.onDisconnect,\n apdu,\n );\n },\n deviceModel: {\n ...this.speculosDevice.deviceModel,\n productName: `Speculos - ${appName} - ${appVersion}`,\n getBlockSize() {\n return 32;\n },\n },\n transport: this.identifier,\n id: \"SpeculosID\", //TODO make it dynamic at creation\n type: \"USB\",\n };\n\n this.connectedDevice = connectedDevice;\n if (!this._isE2E) {\n this.listenForDisconnect(params.onDisconnect, params.deviceId);\n }\n return Right(connectedDevice);\n } catch (error) {\n return Left(new OpeningConnectionError(error as Error));\n }\n }\n\n async disconnect(_params: {\n connectedDevice: TransportConnectedDevice;\n }): Promise<Either<DmkError, void>> {\n this.logger.debug(\"disconnect\");\n this.connectedDevice = null;\n this.clearDisconnectInterval();\n return Promise.resolve(Right(undefined));\n }\n\n async sendApdu(\n _sessionId: string,\n deviceId: DeviceId,\n onDisconnect: DisconnectHandler,\n apdu: Uint8Array,\n ): Promise<Either<DmkError, ApduResponse>> {\n try {\n const hexApdu = bufferToHexaString(apdu).substring(2);\n const hexResponse: string =\n await this._speculosDataSource.postApdu(hexApdu);\n this.logger.debug(formatApduSentLog(apdu));\n const apduResponse = this.createApduResponse(hexResponse);\n this.logger.debug(formatApduReceivedLog(apduResponse));\n return Right(apduResponse);\n } catch (error) {\n if (this.connectedDevice) {\n this.logger.debug(\"disconnecting\");\n onDisconnect(deviceId);\n this.disconnect({\n connectedDevice: this.connectedDevice,\n });\n }\n return Left(new GeneralDmkError(error));\n }\n }\n\n private createApduResponse(hexApdu: string): ApduResponse {\n const apduResponse = {\n statusCode: this.fromHexString(\n hexApdu.substring(hexApdu.length - 4, hexApdu.length),\n ),\n data: this.fromHexString(hexApdu.substring(0, hexApdu.length - 4)),\n };\n return apduResponse;\n }\n\n //TODO: Move this to a helper\n private fromHexString(hexString: string): Uint8Array {\n if (!hexString) {\n return Uint8Array.from([]);\n }\n return new Uint8Array(\n hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),\n );\n }\n\n private listenForDisconnect(\n onDisconnect: DisconnectHandler,\n deviceId: DeviceId,\n ): void {\n this.disconnectInterval = setInterval(async () => {\n const isServerAvailable =\n await this._speculosDataSource.isServerAvailable();\n\n if (!isServerAvailable) {\n this.logger.info(\n `Speculos server unavailable, disconnecting device ${deviceId}`,\n );\n onDisconnect(deviceId);\n\n if (this.connectedDevice) {\n await this.disconnect({\n connectedDevice: this.connectedDevice,\n });\n }\n\n this.clearDisconnectInterval();\n }\n }, 2000);\n }\n\n private clearDisconnectInterval(): void {\n if (this.disconnectInterval) {\n clearInterval(this.disconnectInterval);\n this.disconnectInterval = null;\n }\n }\n}\n\nexport const speculosTransportFactory: (\n speculosUrl?: string,\n isE2E?: boolean,\n deviceModelId?: DeviceModelId,\n options?: SpeculosTransportOptions,\n) => TransportFactory =\n (\n speculosUrl = \"http://127.0.0.1:5000\",\n isE2E = false,\n deviceModelId?,\n options?,\n ) =>\n ({ config, loggerServiceFactory }) =>\n new SpeculosTransport(\n loggerServiceFactory,\n config,\n speculosUrl,\n isE2E,\n deviceModelId,\n options,\n );\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["ApduParser", "bufferToHexaString", "DeviceModelId", "formatApduReceivedLog", "formatApduSentLog", "GeneralDmkError", "OpeningConnectionError", "Left", "Right", "from", "HttpSpeculosDatasource", "speculosIdentifier", "SpeculosTransport", "loggerServiceFactory", "_config", "speculosUrl", "isE2E", "deviceModelId", "bearerToken", "params", "hexResponse", "apduResponse", "parser", "appName", "appVersion", "sessionId", "connectedDevice", "apdu", "error", "_params", "_sessionId", "deviceId", "onDisconnect", "hexApdu", "hexString", "byte", "speculosTransportFactory", "options", "config"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{DeviceModelId as l}from"@ledgerhq/device-management-kit";import{afterEach as f,beforeEach as m,describe as c,expect as t,it as a,vi as e}from"vitest";import{SpeculosTransport as p,speculosTransportFactory as d}from"./SpeculosTransport";import{HttpSpeculosDatasource as i}from"../internal/datasource/HttpSpeculosDatasource";const v="0108457468657265756d0a312e32332e302d64657601009000",u=e.fn(),n=e.fn();e.mock("@internal/datasource/HttpSpeculosDatasource",()=>({HttpSpeculosDatasource:e.fn(()=>({postApdu:u,isServerAvailable:n,openEventStream:e.fn()}))}));const s=()=>({debug:e.fn(),info:e.fn(),warn:e.fn(),error:e.fn()});c("SpeculosTransport",()=>{m(()=>{e.clearAllMocks(),e.useFakeTimers(),u.mockResolvedValue(v),n.mockResolvedValue(!0)}),f(()=>{e.useRealTimers()});const h=async()=>{const o=new p(s,{},"http://localhost:5000"),r=await o.connect({deviceId:"SpeculosID",onDisconnect:e.fn()});return{transport:o,connectedDevice:r.unsafeCoerce()}};c("disconnect",()=>{a("should stop polling the server once disconnected",async()=>{const{transport:o,connectedDevice:r}=await h();await e.advanceTimersByTimeAsync(2e3),t(n).toHaveBeenCalledTimes(1),await o.disconnect({connectedDevice:r}),n.mockClear(),await e.advanceTimersByTimeAsync(1e4),t(n).not.toHaveBeenCalled()})}),c("bearer token",()=>{const o="session-token";a("should pass the bearer token to the datasource",()=>{new p(s,{},"http://localhost:5000",!0,l.NANO_X,{bearerToken:o}),t(i).toHaveBeenCalledWith("http://localhost:5000",void 0,o)}),a("should default to the local Speculos without a bearer token",()=>{d()({config:{},loggerServiceFactory:s}),t(i).toHaveBeenCalledWith("http://127.0.0.1:5000",void 0,void 0)}),a("should pass the bearer token through the factory",()=>{d("http://localhost:5001",!0,l.NANO_X,{bearerToken:o})({config:{},loggerServiceFactory:s}),t(i).toHaveBeenCalledWith("http://localhost:5001",void 0,o)})})});
|
|
2
|
+
//# sourceMappingURL=SpeculosTransport.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/api/SpeculosTransport.test.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n DeviceModelId,\n type DmkConfig,\n type LoggerPublisherService,\n type TransportArgs,\n type TransportConnectedDevice,\n} from \"@ledgerhq/device-management-kit\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport {\n SpeculosTransport,\n speculosTransportFactory,\n} from \"@api/SpeculosTransport\";\nimport { HttpSpeculosDatasource } from \"@internal/datasource/HttpSpeculosDatasource\";\n\nconst GET_APP_AND_VERSION_RESPONSE =\n \"0108457468657265756d0a312e32332e302d64657601009000\";\n\nconst postApdu = vi.fn();\nconst isServerAvailable = vi.fn();\n\nvi.mock(\"@internal/datasource/HttpSpeculosDatasource\", () => ({\n HttpSpeculosDatasource: vi.fn(() => ({\n postApdu,\n isServerAvailable,\n openEventStream: vi.fn(),\n })),\n}));\n\nconst loggerFactory = () =>\n ({\n debug: vi.fn(),\n info: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n }) as unknown as LoggerPublisherService;\n\ndescribe(\"SpeculosTransport\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n vi.useFakeTimers();\n postApdu.mockResolvedValue(GET_APP_AND_VERSION_RESPONSE);\n isServerAvailable.mockResolvedValue(true);\n });\n\n afterEach(() => {\n // Restore real timers so the fake ones cannot leak into another test file.\n vi.useRealTimers();\n });\n\n const connect = async () => {\n const transport = new SpeculosTransport(\n loggerFactory,\n {} as DmkConfig,\n \"http://localhost:5000\",\n );\n const result = await transport.connect({\n deviceId: \"SpeculosID\",\n onDisconnect: vi.fn(),\n });\n return {\n transport,\n connectedDevice: result.unsafeCoerce() as TransportConnectedDevice,\n };\n };\n\n describe(\"disconnect\", () => {\n it(\"should stop polling the server once disconnected\", async () => {\n const { transport, connectedDevice } = await connect();\n await vi.advanceTimersByTimeAsync(2000);\n expect(isServerAvailable).toHaveBeenCalledTimes(1);\n\n await transport.disconnect({ connectedDevice });\n isServerAvailable.mockClear();\n await vi.advanceTimersByTimeAsync(10_000);\n\n expect(isServerAvailable).not.toHaveBeenCalled();\n });\n });\n\n describe(\"bearer token\", () => {\n const bearerToken = \"session-token\";\n\n it(\"should pass the bearer token to the datasource\", () => {\n new SpeculosTransport(\n loggerFactory,\n {} as DmkConfig,\n \"http://localhost:5000\",\n true,\n DeviceModelId.NANO_X,\n { bearerToken },\n );\n\n expect(HttpSpeculosDatasource).toHaveBeenCalledWith(\n \"http://localhost:5000\",\n undefined,\n bearerToken,\n );\n });\n\n it(\"should default to the local Speculos without a bearer token\", () => {\n speculosTransportFactory()({\n config: {} as DmkConfig,\n loggerServiceFactory: loggerFactory,\n } as unknown as TransportArgs);\n\n expect(HttpSpeculosDatasource).toHaveBeenCalledWith(\n \"http://127.0.0.1:5000\",\n undefined,\n undefined,\n );\n });\n\n it(\"should pass the bearer token through the factory\", () => {\n speculosTransportFactory(\n \"http://localhost:5001\",\n true,\n DeviceModelId.NANO_X,\n { bearerToken },\n )({\n config: {} as DmkConfig,\n loggerServiceFactory: loggerFactory,\n } as unknown as TransportArgs);\n\n expect(HttpSpeculosDatasource).toHaveBeenCalledWith(\n \"http://localhost:5001\",\n undefined,\n bearerToken,\n );\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OACE,iBAAAA,MAKK,kCACP,OAAS,aAAAC,EAAW,cAAAC,EAAY,YAAAC,EAAU,UAAAC,EAAQ,MAAAC,EAAI,MAAAC,MAAU,SAEhE,OACE,qBAAAC,EACA,4BAAAC,MACK,yBACP,OAAS,0BAAAC,MAA8B,8CAEvC,MAAMC,EACJ,qDAEIC,EAAWL,EAAG,GAAG,EACjBM,EAAoBN,EAAG,GAAG,EAEhCA,EAAG,KAAK,8CAA+C,KAAO,CAC5D,uBAAwBA,EAAG,GAAG,KAAO,CACnC,SAAAK,EACA,kBAAAC,EACA,gBAAiBN,EAAG,GAAG,CACzB,EAAE,CACJ,EAAE,EAEF,MAAMO,EAAgB,KACnB,CACC,MAAOP,EAAG,GAAG,EACb,KAAMA,EAAG,GAAG,EACZ,KAAMA,EAAG,GAAG,EACZ,MAAOA,EAAG,GAAG,CACf,GAEFH,EAAS,oBAAqB,IAAM,CAClCD,EAAW,IAAM,CACfI,EAAG,cAAc,EACjBA,EAAG,cAAc,EACjBK,EAAS,kBAAkBD,CAA4B,EACvDE,EAAkB,kBAAkB,EAAI,CAC1C,CAAC,EAEDX,EAAU,IAAM,CAEdK,EAAG,cAAc,CACnB,CAAC,EAED,MAAMQ,EAAU,SAAY,CAC1B,MAAMC,EAAY,IAAIR,EACpBM,EACA,CAAC,EACD,uBACF,EACMG,EAAS,MAAMD,EAAU,QAAQ,CACrC,SAAU,aACV,aAAcT,EAAG,GAAG,CACtB,CAAC,EACD,MAAO,CACL,UAAAS,EACA,gBAAiBC,EAAO,aAAa,CACvC,CACF,EAEAb,EAAS,aAAc,IAAM,CAC3BE,EAAG,mDAAoD,SAAY,CACjE,KAAM,CAAE,UAAAU,EAAW,gBAAAE,CAAgB,EAAI,MAAMH,EAAQ,EACrD,MAAMR,EAAG,yBAAyB,GAAI,EACtCF,EAAOQ,CAAiB,EAAE,sBAAsB,CAAC,EAEjD,MAAMG,EAAU,WAAW,CAAE,gBAAAE,CAAgB,CAAC,EAC9CL,EAAkB,UAAU,EAC5B,MAAMN,EAAG,yBAAyB,GAAM,EAExCF,EAAOQ,CAAiB,EAAE,IAAI,iBAAiB,CACjD,CAAC,CACH,CAAC,EAEDT,EAAS,eAAgB,IAAM,CAC7B,MAAMe,EAAc,gBAEpBb,EAAG,iDAAkD,IAAM,CACzD,IAAIE,EACFM,EACA,CAAC,EACD,wBACA,GACAb,EAAc,OACd,CAAE,YAAAkB,CAAY,CAChB,EAEAd,EAAOK,CAAsB,EAAE,qBAC7B,wBACA,OACAS,CACF,CACF,CAAC,EAEDb,EAAG,8DAA+D,IAAM,CACtEG,EAAyB,EAAE,CACzB,OAAQ,CAAC,EACT,qBAAsBK,CACxB,CAA6B,EAE7BT,EAAOK,CAAsB,EAAE,qBAC7B,wBACA,OACA,MACF,CACF,CAAC,EAEDJ,EAAG,mDAAoD,IAAM,CAC3DG,EACE,wBACA,GACAR,EAAc,OACd,CAAE,YAAAkB,CAAY,CAChB,EAAE,CACA,OAAQ,CAAC,EACT,qBAAsBL,CACxB,CAA6B,EAE7BT,EAAOK,CAAsB,EAAE,qBAC7B,wBACA,OACAS,CACF,CACF,CAAC,CACH,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["DeviceModelId", "afterEach", "beforeEach", "describe", "expect", "it", "vi", "SpeculosTransport", "speculosTransportFactory", "HttpSpeculosDatasource", "GET_APP_AND_VERSION_RESPONSE", "postApdu", "isServerAvailable", "loggerFactory", "connect", "transport", "result", "connectedDevice", "bearerToken"]
|
|
7
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{DmkNetworkClient as w}from"@ledgerhq/device-management-kit";import b from"../../../package.json";const v=2e3,S=s=>{let t=s.length;for(;t>0&&s[t-1]==="/";)t--;return s.slice(0,t)};class D{baseUrl;headers;http;constructor(t,a=`ldmk-transport-speculos/${b.version}`,o){this.baseUrl=S(t),this.headers={"X-Ledger-Client-Version":a,...o?{Authorization:`Bearer ${o}`}:{}},this.http=new w({headers:this.headers})}async postApdu(t){return(await this.http.post(`${this.baseUrl}/apdu`,{data:t})).data}async isServerAvailable(){try{return await this.http.get(`${this.baseUrl}/events`,{timeoutMs:v,responseType:"void"}),!0}catch{return!1}}async openEventStream(t,a){if(typeof fetch>"u")throw new Error("global fetch is not available in Node < 18");const o=`${this.baseUrl}/events?stream=true`,i=new AbortController,u={...this.headers,Accept:"text/event-stream","Cache-Control":"no-cache"},c=await fetch(o,{method:"GET",headers:u,signal:i.signal});if(!c.ok)throw i.abort(),new Error(`SSE request failed with status ${c.status}`);const l=c.body;if(!l)throw i.abort(),new Error("SSE response has no body stream.");const f=l.getReader(),m=new TextDecoder("utf-8");let e="",d=!1;const g=()=>{if(!d){d=!0;try{a?.()}catch{}}},h=r=>{if(r.startsWith("data: ")){const n=r.slice(6);try{t(JSON.parse(n))}catch{t({data:n})}}};return(async()=>{try{for(;;){const{value:r,done:n}=await f.read();if(n)break;e+=m.decode(r,{stream:!0});const p=e.split(/\r?\n/);e=p.pop()??"";for(const y of p)h(y)}}catch{}finally{if(e.length){for(const r of e.split(/\r?\n/))h(r);e=""}g()}})(),l}}export{D as HttpSpeculosDatasource};
|
|
2
2
|
//# sourceMappingURL=HttpSpeculosDatasource.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/internal/datasource/HttpSpeculosDatasource.ts"],
|
|
4
|
-
"sourcesContent": ["import
|
|
5
|
-
"mappings": "AAAA,
|
|
6
|
-
"names": ["
|
|
4
|
+
"sourcesContent": ["import { DmkNetworkClient } from \"@ledgerhq/device-management-kit\";\n\nimport PACKAGE from \"@root/package.json\";\n\nimport { type SpeculosDatasource } from \"./SpeculosDatasource\";\n\nconst TIMEOUT = 2000; // 2 second timeout for availability check\n\n// A loop rather than /\\/+$/, which backtracks quadratically on long runs of slashes.\nconst removeTrailingSlashes = (url: string) => {\n let end = url.length;\n while (end > 0 && url[end - 1] === \"/\") end--;\n return url.slice(0, end);\n};\n\nexport class HttpSpeculosDatasource implements SpeculosDatasource {\n private readonly baseUrl: string;\n private readonly headers: Record<string, string>;\n private readonly http: DmkNetworkClient;\n\n constructor(\n baseUrl: string,\n clientHeader: string = `ldmk-transport-speculos/${PACKAGE.version}`,\n bearerToken?: string,\n ) {\n this.baseUrl = removeTrailingSlashes(baseUrl);\n this.headers = {\n \"X-Ledger-Client-Version\": clientHeader,\n ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),\n };\n this.http = new DmkNetworkClient({ headers: this.headers });\n }\n\n async postApdu(apdu: string): Promise<string> {\n const data = (await this.http.post(`${this.baseUrl}/apdu`, {\n data: apdu,\n })) as SpeculosApduDTO;\n return data.data;\n }\n\n async isServerAvailable(): Promise<boolean> {\n try {\n await this.http.get(`${this.baseUrl}/events`, {\n timeoutMs: TIMEOUT,\n responseType: \"void\",\n });\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * open an SSE event stream using the fetch API.\n * - calls `onEvent` for each line starting with \"data: \"\n * - calls `onClose` when the stream ends or errors\n * - returns the ReadableStream so callers can `cancel()` it\n */\n async openEventStream(\n onEvent: (json: Record<string, unknown>) => void,\n onClose?: () => void,\n ): Promise<ReadableStream<Uint8Array>> {\n if (typeof fetch === \"undefined\") {\n throw new Error(\"global fetch is not available in Node < 18\");\n }\n\n const url = `${this.baseUrl}/events?stream=true`;\n\n const controller = new AbortController();\n\n const headers: HeadersInit = {\n ...this.headers,\n Accept: \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n };\n\n const response = await fetch(url, {\n method: \"GET\",\n headers,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n controller.abort();\n throw new Error(`SSE request failed with status ${response.status}`);\n }\n\n const stream = response.body;\n if (!stream) {\n controller.abort();\n throw new Error(\"SSE response has no body stream.\");\n }\n\n const reader = stream.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let buffer = \"\";\n let closed = false;\n\n const finalize = () => {\n if (!closed) {\n closed = true;\n try {\n onClose?.();\n } catch {\n // swallow listener errors\n }\n }\n };\n\n const emitParsedEvent = (line: string) => {\n if (line.startsWith(\"data: \")) {\n const payload = line.slice(6);\n try {\n onEvent(JSON.parse(payload));\n } catch {\n onEvent({ data: payload });\n }\n }\n };\n\n (async () => {\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // normalise line breaks and process complete lines\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() ?? \"\"; // keep last partial line\n\n for (const line of lines) {\n emitParsedEvent(line);\n // other SSE fields ignored to mirror original behavior\n }\n }\n } catch {\n // network/reader error\n } finally {\n // flush any remaining buffered text as lines\n if (buffer.length) {\n for (const line of buffer.split(/\\r?\\n/)) {\n emitParsedEvent(line);\n }\n buffer = \"\";\n }\n finalize();\n }\n })();\n\n // consumers can cancel with: (await openEventStream(...)).cancel()\n return stream;\n }\n}\n\ntype SpeculosApduDTO = {\n data: string;\n};\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,oBAAAA,MAAwB,kCAEjC,OAAOC,MAAa,qBAIpB,MAAMC,EAAU,IAGVC,EAAyBC,GAAgB,CAC7C,IAAIC,EAAMD,EAAI,OACd,KAAOC,EAAM,GAAKD,EAAIC,EAAM,CAAC,IAAM,KAAKA,IACxC,OAAOD,EAAI,MAAM,EAAGC,CAAG,CACzB,EAEO,MAAMC,CAAqD,CAC/C,QACA,QACA,KAEjB,YACEC,EACAC,EAAuB,2BAA2BP,EAAQ,OAAO,GACjEQ,EACA,CACA,KAAK,QAAUN,EAAsBI,CAAO,EAC5C,KAAK,QAAU,CACb,0BAA2BC,EAC3B,GAAIC,EAAc,CAAE,cAAe,UAAUA,CAAW,EAAG,EAAI,CAAC,CAClE,EACA,KAAK,KAAO,IAAIT,EAAiB,CAAE,QAAS,KAAK,OAAQ,CAAC,CAC5D,CAEA,MAAM,SAASU,EAA+B,CAI5C,OAHc,MAAM,KAAK,KAAK,KAAK,GAAG,KAAK,OAAO,QAAS,CACzD,KAAMA,CACR,CAAC,GACW,IACd,CAEA,MAAM,mBAAsC,CAC1C,GAAI,CACF,aAAM,KAAK,KAAK,IAAI,GAAG,KAAK,OAAO,UAAW,CAC5C,UAAWR,EACX,aAAc,MAChB,CAAC,EACM,EACT,MAAQ,CACN,MAAO,EACT,CACF,CAQA,MAAM,gBACJS,EACAC,EACqC,CACrC,GAAI,OAAO,MAAU,IACnB,MAAM,IAAI,MAAM,4CAA4C,EAG9D,MAAMR,EAAM,GAAG,KAAK,OAAO,sBAErBS,EAAa,IAAI,gBAEjBC,EAAuB,CAC3B,GAAG,KAAK,QACR,OAAQ,oBACR,gBAAiB,UACnB,EAEMC,EAAW,MAAM,MAAMX,EAAK,CAChC,OAAQ,MACR,QAAAU,EACA,OAAQD,EAAW,MACrB,CAAC,EAED,GAAI,CAACE,EAAS,GACZ,MAAAF,EAAW,MAAM,EACX,IAAI,MAAM,kCAAkCE,EAAS,MAAM,EAAE,EAGrE,MAAMC,EAASD,EAAS,KACxB,GAAI,CAACC,EACH,MAAAH,EAAW,MAAM,EACX,IAAI,MAAM,kCAAkC,EAGpD,MAAMI,EAASD,EAAO,UAAU,EAC1BE,EAAU,IAAI,YAAY,OAAO,EACvC,IAAIC,EAAS,GACTC,EAAS,GAEb,MAAMC,EAAW,IAAM,CACrB,GAAI,CAACD,EAAQ,CACXA,EAAS,GACT,GAAI,CACFR,IAAU,CACZ,MAAQ,CAER,CACF,CACF,EAEMU,EAAmBC,GAAiB,CACxC,GAAIA,EAAK,WAAW,QAAQ,EAAG,CAC7B,MAAMC,EAAUD,EAAK,MAAM,CAAC,EAC5B,GAAI,CACFZ,EAAQ,KAAK,MAAMa,CAAO,CAAC,CAC7B,MAAQ,CACNb,EAAQ,CAAE,KAAMa,CAAQ,CAAC,CAC3B,CACF,CACF,EAEA,OAAC,SAAY,CACX,GAAI,CACF,OAAa,CACX,KAAM,CAAE,MAAAC,EAAO,KAAAC,CAAK,EAAI,MAAMT,EAAO,KAAK,EAC1C,GAAIS,EAAM,MACVP,GAAUD,EAAQ,OAAOO,EAAO,CAAE,OAAQ,EAAK,CAAC,EAGhD,MAAME,EAAQR,EAAO,MAAM,OAAO,EAClCA,EAASQ,EAAM,IAAI,GAAK,GAExB,UAAWJ,KAAQI,EACjBL,EAAgBC,CAAI,CAGxB,CACF,MAAQ,CAER,QAAE,CAEA,GAAIJ,EAAO,OAAQ,CACjB,UAAWI,KAAQJ,EAAO,MAAM,OAAO,EACrCG,EAAgBC,CAAI,EAEtBJ,EAAS,EACX,CACAE,EAAS,CACX,CACF,GAAG,EAGIL,CACT,CACF",
|
|
6
|
+
"names": ["DmkNetworkClient", "PACKAGE", "TIMEOUT", "removeTrailingSlashes", "url", "end", "HttpSpeculosDatasource", "baseUrl", "clientHeader", "bearerToken", "apdu", "onEvent", "onClose", "controller", "headers", "response", "stream", "reader", "decoder", "buffer", "closed", "finalize", "emitParsedEvent", "line", "payload", "value", "done", "lines"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{afterEach as r,describe as d,expect as e,it as a,vi as o}from"vitest";import{HttpSpeculosDatasource as s}from"./HttpSpeculosDatasource";const c="session-token",i={Authorization:`Bearer ${c}`},l=t=>new Response(JSON.stringify(t),{headers:{"Content-Type":"application/json"}});r(()=>{o.restoreAllMocks()});d("HttpSpeculosDatasource",()=>{a("sends only the client header by default",async()=>{const t=o.spyOn(globalThis,"fetch").mockResolvedValue(l({data:"9000"}));await new s("http://speculos/","client").postApdu("b001000000"),e(t).toHaveBeenCalledWith("http://speculos/apdu",e.objectContaining({method:"POST",headers:e.objectContaining({"X-Ledger-Client-Version":"client"})}));const[,n]=t.mock.calls[0];e(n?.headers).not.toHaveProperty("Authorization")}),a("strips every trailing slash from the base URL",async()=>{const t=o.spyOn(globalThis,"fetch").mockImplementation(()=>Promise.resolve(l({data:"9000"})));await new s("http://speculos///").postApdu("b0"),await new s("///").postApdu("b0"),e(t.mock.calls.map(([n])=>n)).toEqual(["http://speculos/apdu","/apdu"])}),a("sends the bearer token with APDUs",async()=>{const t=o.spyOn(globalThis,"fetch").mockResolvedValue(l({data:"9000"})),n=await new s("http://speculos","client",c).postApdu("b001000000");e(n).toBe("9000"),e(t).toHaveBeenCalledWith("http://speculos/apdu",e.objectContaining({headers:e.objectContaining({...i,"X-Ledger-Client-Version":"client"})}))}),a("sends the bearer token with the availability check",async()=>{const t=o.spyOn(globalThis,"fetch").mockResolvedValue(new Response(null)),n=await new s("http://speculos","client",c).isServerAvailable();e(n).toBe(!0),e(t).toHaveBeenCalledWith("http://speculos/events",e.objectContaining({method:"GET",headers:e.objectContaining(i)}))}),a("sends the bearer token with the event stream",async()=>{const t=o.spyOn(globalThis,"fetch").mockResolvedValue(new Response(`data: {"text":"Ready"}
|
|
2
|
+
|
|
3
|
+
`,{headers:{"Content-Type":"text/event-stream"}})),n=o.fn();await new Promise((p,h)=>{new s("http://speculos","client",c).openEventStream(n,p).catch(h)}),e(t).toHaveBeenCalledWith("http://speculos/events?stream=true",e.objectContaining({headers:{...i,"X-Ledger-Client-Version":"client",Accept:"text/event-stream","Cache-Control":"no-cache"}})),e(n).toHaveBeenCalledWith({text:"Ready"})})});
|
|
4
|
+
//# sourceMappingURL=HttpSpeculosDatasource.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/internal/datasource/HttpSpeculosDatasource.test.ts"],
|
|
4
|
+
"sourcesContent": ["import { afterEach, describe, expect, it, vi } from \"vitest\";\n\nimport { HttpSpeculosDatasource } from \"./HttpSpeculosDatasource\";\n\nconst TOKEN = \"session-token\";\nconst AUTHORIZATION = { Authorization: `Bearer ${TOKEN}` };\n\nconst jsonResponse = (body: unknown) =>\n new Response(JSON.stringify(body), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n\nafterEach(() => {\n vi.restoreAllMocks();\n});\n\ndescribe(\"HttpSpeculosDatasource\", () => {\n it(\"sends only the client header by default\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockResolvedValue(jsonResponse({ data: \"9000\" }));\n\n await new HttpSpeculosDatasource(\"http://speculos/\", \"client\").postApdu(\n \"b001000000\",\n );\n\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/apdu\",\n expect.objectContaining({\n method: \"POST\",\n headers: expect.objectContaining({\n \"X-Ledger-Client-Version\": \"client\",\n }),\n }),\n );\n const [, init] = fetchSpy.mock.calls[0]!;\n expect(init?.headers).not.toHaveProperty(\"Authorization\");\n });\n\n it(\"strips every trailing slash from the base URL\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockImplementation(() =>\n Promise.resolve(jsonResponse({ data: \"9000\" })),\n );\n\n await new HttpSpeculosDatasource(\"http://speculos///\").postApdu(\"b0\");\n await new HttpSpeculosDatasource(\"///\").postApdu(\"b0\");\n\n expect(fetchSpy.mock.calls.map(([url]) => url)).toEqual([\n \"http://speculos/apdu\",\n \"/apdu\",\n ]);\n });\n\n it(\"sends the bearer token with APDUs\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockResolvedValue(jsonResponse({ data: \"9000\" }));\n\n const response = await new HttpSpeculosDatasource(\n \"http://speculos\",\n \"client\",\n TOKEN,\n ).postApdu(\"b001000000\");\n\n expect(response).toBe(\"9000\");\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/apdu\",\n expect.objectContaining({\n headers: expect.objectContaining({\n ...AUTHORIZATION,\n \"X-Ledger-Client-Version\": \"client\",\n }),\n }),\n );\n });\n\n it(\"sends the bearer token with the availability check\", async () => {\n const fetchSpy = vi\n .spyOn(globalThis, \"fetch\")\n .mockResolvedValue(new Response(null));\n\n const available = await new HttpSpeculosDatasource(\n \"http://speculos\",\n \"client\",\n TOKEN,\n ).isServerAvailable();\n\n expect(available).toBe(true);\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/events\",\n expect.objectContaining({\n method: \"GET\",\n headers: expect.objectContaining(AUTHORIZATION),\n }),\n );\n });\n\n it(\"sends the bearer token with the event stream\", async () => {\n const fetchSpy = vi.spyOn(globalThis, \"fetch\").mockResolvedValue(\n new Response('data: {\"text\":\"Ready\"}\\n\\n', {\n headers: { \"Content-Type\": \"text/event-stream\" },\n }),\n );\n const onEvent = vi.fn();\n\n await new Promise<void>((resolve, reject) => {\n new HttpSpeculosDatasource(\"http://speculos\", \"client\", TOKEN)\n .openEventStream(onEvent, resolve)\n .catch(reject);\n });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n \"http://speculos/events?stream=true\",\n expect.objectContaining({\n headers: {\n ...AUTHORIZATION,\n \"X-Ledger-Client-Version\": \"client\",\n Accept: \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n },\n }),\n );\n expect(onEvent).toHaveBeenCalledWith({ text: \"Ready\" });\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["afterEach", "describe", "expect", "it", "vi", "HttpSpeculosDatasource", "TOKEN", "AUTHORIZATION", "jsonResponse", "body", "fetchSpy", "init", "url", "response", "available", "onEvent", "resolve", "reject"]
|
|
7
|
+
}
|
|
@@ -2,6 +2,9 @@ import { type ApduResponse, type ConnectError, type DeviceId, DeviceModelId, typ
|
|
|
2
2
|
import { type Either } from "purify-ts";
|
|
3
3
|
import { type Observable } from "rxjs";
|
|
4
4
|
export declare const speculosIdentifier: TransportIdentifier;
|
|
5
|
+
export type SpeculosTransportOptions = {
|
|
6
|
+
bearerToken?: string;
|
|
7
|
+
};
|
|
5
8
|
export declare class SpeculosTransport implements Transport {
|
|
6
9
|
private logger;
|
|
7
10
|
private readonly identifier;
|
|
@@ -10,7 +13,7 @@ export declare class SpeculosTransport implements Transport {
|
|
|
10
13
|
private disconnectInterval;
|
|
11
14
|
private readonly _isE2E;
|
|
12
15
|
private readonly speculosDevice;
|
|
13
|
-
constructor(loggerServiceFactory: (tag: string) => LoggerPublisherService, _config: DmkConfig, speculosUrl: string, isE2E?: boolean, deviceModelId?: DeviceModelId);
|
|
16
|
+
constructor(loggerServiceFactory: (tag: string) => LoggerPublisherService, _config: DmkConfig, speculosUrl: string, isE2E?: boolean, deviceModelId?: DeviceModelId, { bearerToken }?: SpeculosTransportOptions);
|
|
14
17
|
isSupported(): boolean;
|
|
15
18
|
getIdentifier(): TransportIdentifier;
|
|
16
19
|
listenToAvailableDevices(): Observable<TransportDiscoveredDevice[]>;
|
|
@@ -27,6 +30,7 @@ export declare class SpeculosTransport implements Transport {
|
|
|
27
30
|
private createApduResponse;
|
|
28
31
|
private fromHexString;
|
|
29
32
|
private listenForDisconnect;
|
|
33
|
+
private clearDisconnectInterval;
|
|
30
34
|
}
|
|
31
|
-
export declare const speculosTransportFactory: (speculosUrl?: string, isE2E?: boolean, deviceModelId?: DeviceModelId) => TransportFactory;
|
|
35
|
+
export declare const speculosTransportFactory: (speculosUrl?: string, isE2E?: boolean, deviceModelId?: DeviceModelId, options?: SpeculosTransportOptions) => TransportFactory;
|
|
32
36
|
//# sourceMappingURL=SpeculosTransport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpeculosTransport.d.ts","sourceRoot":"","sources":["../../../../src/api/SpeculosTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,QAAQ,EAIb,KAAK,sBAAsB,EAE3B,KAAK,SAAS,EACd,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,KAAK,MAAM,EAAe,MAAM,WAAW,CAAC;AACrD,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAK7C,eAAO,MAAM,kBAAkB,EAAE,mBACN,CAAC;AAE5B,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2C;IACtE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;gBAGzD,oBAAoB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,sBAAsB,EAC7D,OAAO,EAAE,SAAS,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,CAAC,EAAE,OAAO,EACf,aAAa,GAAE,aAAkC;
|
|
1
|
+
{"version":3,"file":"SpeculosTransport.d.ts","sourceRoot":"","sources":["../../../../src/api/SpeculosTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,QAAQ,EAIb,KAAK,sBAAsB,EAE3B,KAAK,SAAS,EACd,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,KAAK,MAAM,EAAe,MAAM,WAAW,CAAC;AACrD,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAK7C,eAAO,MAAM,kBAAkB,EAAE,mBACN,CAAC;AAE5B,MAAM,MAAM,wBAAwB,GAAG;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2C;IACtE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;gBAGzD,oBAAoB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,sBAAsB,EAC7D,OAAO,EAAE,SAAS,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,CAAC,EAAE,OAAO,EACf,aAAa,GAAE,aAAkC,EACjD,EAAE,WAAW,EAAE,GAAE,wBAA6B;IA2BhD,WAAW,IAAI,OAAO;IAItB,aAAa,IAAI,mBAAmB;IAIpC,wBAAwB,IAAI,UAAU,CAAC,yBAAyB,EAAE,CAAC;IAInE,gBAAgB,IAAI,UAAU,CAAC,yBAAyB,CAAC;IAKzD,eAAe,IAAI,IAAI;IAKjB,OAAO,CAAC,MAAM,EAAE;QACpB,QAAQ,EAAE,QAAQ,CAAC;QACnB,YAAY,EAAE,iBAAiB,CAAC;KACjC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAC;IAgDrD,UAAU,CAAC,OAAO,EAAE;QACxB,eAAe,EAAE,wBAAwB,CAAC;KAC3C,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAO7B,QAAQ,CACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,iBAAiB,EAC/B,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAqB1C,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,uBAAuB;CAMhC;AAED,eAAO,MAAM,wBAAwB,EAAE,CACrC,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,OAAO,EACf,aAAa,CAAC,EAAE,aAAa,EAC7B,OAAO,CAAC,EAAE,wBAAwB,KAC/B,gBAeA,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpeculosTransport.test.d.ts","sourceRoot":"","sources":["../../../../src/api/SpeculosTransport.test.ts"],"names":[],"mappings":""}
|