or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

tessl/npm-ledgerhq--hw-transport-webusb

Ledger Hardware Wallet WebUSB implementation of the communication layer for web browsers

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/@ledgerhq/hw-transport-webusb@6.29.x

To install, run

npx @tessl/cli install tessl/npm-ledgerhq--hw-transport-webusb@6.29.0

0

# Ledger WebUSB Transport

1

2

Ledger WebUSB Transport provides a WebUSB-based communication layer for interacting with Ledger Hardware Wallets in web browsers. It enables secure APDU (Application Protocol Data Unit) exchange between web applications and Ledger devices through the WebUSB API, handling device discovery, connection management, and protocol-level communication.

3

4

## Package Information

5

6

- **Package Name**: @ledgerhq/hw-transport-webusb

7

- **Package Type**: npm

8

- **Language**: TypeScript

9

- **Installation**: `npm install @ledgerhq/hw-transport-webusb`

10

- **Browser Requirements**: WebUSB support (Chrome/Chromium-based browsers), HTTPS required

11

12

## Core Imports

13

14

```typescript

15

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

16

```

17

18

For CommonJS:

19

20

```javascript

21

const TransportWebUSB = require("@ledgerhq/hw-transport-webusb").default;

22

```

23

24

Named imports for error classes:

25

26

```typescript

27

import TransportWebUSB, {

28

TransportOpenUserCancelled,

29

TransportInterfaceNotAvailable,

30

TransportWebUSBGestureRequired,

31

DisconnectedDeviceDuringOperation,

32

DisconnectedDevice,

33

TransportError,

34

TransportStatusError,

35

StatusCodes,

36

getAltStatusMessage

37

} from "@ledgerhq/hw-transport-webusb";

38

```

39

40

## Basic Usage

41

42

```typescript

43

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

44

45

// Check if WebUSB is supported

46

const isSupported = await TransportWebUSB.isSupported();

47

48

if (isSupported) {

49

// Create a transport connection (shows permission dialog if needed)

50

const transport = await TransportWebUSB.create();

51

52

// Exchange APDU with the device

53

const apdu = Buffer.from("E0C4000000", "hex"); // Get app name APDU

54

const response = await transport.exchange(apdu);

55

56

// Close the connection

57

await transport.close();

58

}

59

```

60

61

## Architecture

62

63

The Ledger WebUSB Transport is built around several key components:

64

65

- **TransportWebUSB Class**: Main transport implementation extending the base Transport class

66

- **Device Management**: Static methods for device discovery, permission handling, and connection establishment

67

- **APDU Protocol**: Low-level APDU exchange with HID framing for USB communication

68

- **Event System**: Disconnect detection and transport lifecycle events

69

- **Error Handling**: Comprehensive error types for various failure scenarios

70

71

## Capabilities

72

73

### Transport Creation and Management

74

75

Core functionality for establishing and managing WebUSB connections to Ledger devices.

76

77

```typescript { .api }

78

/**

79

* Main WebUSB transport class for Ledger devices

80

*/

81

export default class TransportWebUSB extends Transport {

82

constructor(device: USBDevice, interfaceNumber: number);

83

84

/** The connected USB device */

85

device: USBDevice;

86

87

/** Identified device model information */

88

deviceModel: DeviceModel | null | undefined;

89

90

/** Communication channel identifier */

91

channel: number;

92

93

/** USB packet size in bytes */

94

packetSize: number;

95

96

/** USB interface number */

97

interfaceNumber: number;

98

}

99

```

100

101

### Static Device Management Methods

102

103

Methods for checking support, discovering devices, and establishing connections.

104

105

```typescript { .api }

106

/**

107

* Check if WebUSB transport is supported in the current browser

108

* @returns Promise resolving to boolean indicating support

109

*/

110

static isSupported(): Promise<boolean>;

111

112

/**

113

* List WebUSB devices that were previously authorized by the user

114

* @returns Promise resolving to array of authorized USBDevice objects

115

*/

116

static list(): Promise<USBDevice[]>;

117

118

/**

119

* Actively listen to WebUSB devices and emit ONE device

120

* Important: Must be called in the context of a UI click

121

* @param observer - Observer for device descriptor events

122

* @returns Subscription object with unsubscribe method

123

*/

124

static listen(observer: Observer<DescriptorEvent<USBDevice>>): Subscription;

125

126

/**

127

* Always display device permission dialog, even if devices are already accepted

128

* @returns Promise resolving to new TransportWebUSB instance

129

*/

130

static request(): Promise<TransportWebUSB>;

131

132

/**

133

* Never display device permission dialog, returns null if no device found

134

* @returns Promise resolving to TransportWebUSB instance or null

135

*/

136

static openConnected(): Promise<TransportWebUSB | null>;

137

138

/**

139

* Create a Ledger transport with a specific USBDevice

140

* @param device - The USBDevice to create transport for

141

* @returns Promise resolving to new TransportWebUSB instance

142

*/

143

static open(device: USBDevice): Promise<TransportWebUSB>;

144

145

/**

146

* Create a transport (inherited from base Transport class)

147

* @param openTimeout - Timeout for opening connection

148

* @param listenTimeout - Timeout for listening to devices

149

* @returns Promise resolving to new TransportWebUSB instance

150

*/

151

static create(openTimeout?: number, listenTimeout?: number): Promise<TransportWebUSB>;

152

```

153

154

### APDU Communication

155

156

Low-level APDU exchange functionality for communicating with Ledger device applications.

157

158

```typescript { .api }

159

/**

160

* Exchange APDU with the device using the WebUSB protocol

161

* @param apdu - The APDU buffer to send to the device

162

* @returns Promise resolving to response APDU buffer

163

*/

164

exchange(apdu: Buffer): Promise<Buffer>;

165

166

/**

167

* Release the transport device and close the connection

168

* @returns Promise that resolves when connection is closed

169

*/

170

close(): Promise<void>;

171

172

/**

173

* Legacy method for scramble key (no-op implementation)

174

*/

175

setScrambleKey(): void;

176

```

177

178

### High-Level Transport Methods

179

180

Inherited methods from the base Transport class for higher-level APDU operations.

181

182

```typescript { .api }

183

/**

184

* Send APDU command with automatic status code handling

185

* @param cla - Class byte

186

* @param ins - Instruction byte

187

* @param p1 - Parameter 1

188

* @param p2 - Parameter 2

189

* @param data - Optional data buffer

190

* @param statusList - List of acceptable status codes

191

* @param options - Send options

192

* @returns Promise resolving to response data

193

*/

194

send(

195

cla: number,

196

ins: number,

197

p1: number,

198

p2: number,

199

data?: Buffer,

200

statusList?: number[],

201

options?: SendOptions

202

): Promise<Buffer>;

203

204

/**

205

* Set timeout for APDU exchanges

206

* @param exchangeTimeout - Timeout in milliseconds

207

*/

208

setExchangeTimeout(exchangeTimeout: number): void;

209

210

/**

211

* Set timeout for detecting unresponsive devices

212

* @param exchangeUnresponsiveTimeout - Timeout in milliseconds

213

*/

214

setExchangeUnresponsiveTimeout(exchangeUnresponsiveTimeout: number): void;

215

```

216

217

### Event Handling

218

219

Event system for monitoring transport state and device connectivity.

220

221

```typescript { .api }

222

/**

223

* Register event listener

224

* @param eventName - Event name ("disconnect", "unresponsive", "responsive")

225

* @param callback - Event callback function

226

*/

227

on(eventName: string, callback: (...args: any[]) => void): void;

228

229

/**

230

* Remove event listener

231

* @param eventName - Event name

232

* @param callback - Event callback function to remove

233

*/

234

off(eventName: string, callback: (...args: any[]) => void): void;

235

236

/**

237

* Emit event to all registered listeners

238

* @param eventName - Event name

239

* @param args - Event arguments

240

*/

241

emit(eventName: string, ...args: any[]): void;

242

```

243

244

## Types

245

246

### Core Interfaces

247

248

```typescript { .api }

249

/**

250

* Observer pattern interface for device events

251

*/

252

interface Observer<T> {

253

next: (event: T) => unknown;

254

error: (e: unknown) => unknown;

255

complete: () => unknown;

256

}

257

258

/**

259

* Subscription interface for managing event listeners

260

*/

261

interface Subscription {

262

unsubscribe: () => void;

263

}

264

265

/**

266

* Device descriptor event for device add/remove notifications

267

*/

268

interface DescriptorEvent<T> {

269

type: "add" | "remove";

270

descriptor: T;

271

deviceModel?: DeviceModel | null | undefined;

272

}

273

274

/**

275

* Options for send method

276

*/

277

interface SendOptions {

278

/** Timeout for the operation in milliseconds */

279

abortTimeoutMs?: number;

280

}

281

```

282

283

### Device Model Information

284

285

```typescript { .api }

286

/**

287

* Device model enumeration

288

*/

289

enum DeviceModelId {

290

blue = "blue",

291

nanoS = "nanoS",

292

nanoSP = "nanoSP",

293

nanoX = "nanoX",

294

stax = "stax",

295

europa = "europa",

296

apex = "apex"

297

}

298

299

/**

300

* Device model information interface

301

*/

302

interface DeviceModel {

303

id: DeviceModelId;

304

productName: string;

305

productIdMM: number;

306

legacyUsbProductId: number;

307

usbOnly: boolean;

308

memorySize: number;

309

masks: number[];

310

getBlockSize: (firmwareVersion: string) => number;

311

bluetoothSpec?: Array<{

312

serviceUuid: string;

313

writeUuid: string;

314

writeCmdUuid: string;

315

notifyUuid: string;

316

}>;

317

}

318

```

319

320

### Error Types

321

322

```typescript { .api }

323

/**

324

* Base transport error class

325

*/

326

class TransportError extends Error {

327

id: string;

328

}

329

330

/**

331

* APDU status code error

332

*/

333

class TransportStatusError extends Error {

334

statusCode: number;

335

statusText: string;

336

}

337

338

/**

339

* User cancelled device selection

340

*/

341

class TransportOpenUserCancelled extends Error {}

342

343

/**

344

* WebUSB interface not available or not supported

345

*/

346

class TransportInterfaceNotAvailable extends Error {}

347

348

/**

349

* WebUSB operation requires user gesture (click)

350

*/

351

class TransportWebUSBGestureRequired extends Error {}

352

353

/**

354

* Device disconnected during operation

355

*/

356

class DisconnectedDeviceDuringOperation extends Error {}

357

358

/**

359

* Device disconnected

360

*/

361

class DisconnectedDevice extends Error {}

362

363

/**

364

* Transport race condition detected

365

*/

366

class TransportRaceCondition extends Error {}

367

```

368

369

### Constants

370

371

```typescript { .api }

372

/**

373

* Ledger USB vendor ID

374

*/

375

const ledgerUSBVendorId = 0x2c97;

376

377

/**

378

* APDU status codes

379

*/

380

const StatusCodes = {

381

ACCESS_CONDITION_NOT_FULFILLED: 0x9804,

382

ALGORITHM_NOT_SUPPORTED: 0x9484,

383

CLA_NOT_SUPPORTED: 0x6e00,

384

CODE_BLOCKED: 0x9840,

385

CODE_NOT_INITIALIZED: 0x9802,

386

COMMAND_INCOMPATIBLE_FILE_STRUCTURE: 0x6981,

387

CONDITIONS_OF_USE_NOT_SATISFIED: 0x6985,

388

CONTRADICTION_INVALIDATION: 0x9810,

389

CONTRADICTION_SECRET_CODE_STATUS: 0x9808,

390

DEVICE_IN_RECOVERY_MODE: 0x662f,

391

CUSTOM_IMAGE_EMPTY: 0x662e,

392

FILE_ALREADY_EXISTS: 0x6a89,

393

FILE_NOT_FOUND: 0x9404,

394

GP_AUTH_FAILED: 0x6300,

395

HALTED: 0x6faa,

396

INCONSISTENT_FILE: 0x9408,

397

INCORRECT_DATA: 0x6a80,

398

INCORRECT_LENGTH: 0x6700,

399

INCORRECT_P1_P2: 0x6b00,

400

INS_NOT_SUPPORTED: 0x6d00,

401

DEVICE_NOT_ONBOARDED: 0x6d07,

402

DEVICE_NOT_ONBOARDED_2: 0x6611,

403

INVALID_KCV: 0x9485,

404

INVALID_OFFSET: 0x9402,

405

LICENSING: 0x6f42,

406

LOCKED_DEVICE: 0x5515,

407

MAX_VALUE_REACHED: 0x9850,

408

MEMORY_PROBLEM: 0x9240,

409

MISSING_CRITICAL_PARAMETER: 0x6800,

410

NO_EF_SELECTED: 0x9400,

411

NOT_ENOUGH_MEMORY_SPACE: 0x6a84,

412

OK: 0x9000,

413

PIN_REMAINING_ATTEMPTS: 0x63c0,

414

REFERENCED_DATA_NOT_FOUND: 0x6a88,

415

SECURITY_STATUS_NOT_SATISFIED: 0x6982,

416

TECHNICAL_PROBLEM: 0x6f00,

417

UNKNOWN_APDU: 0x6d02,

418

USER_REFUSED_ON_DEVICE: 0x5501,

419

NOT_ENOUGH_SPACE: 0x5102,

420

APP_NOT_FOUND_OR_INVALID_CONTEXT: 0x5123,

421

INVALID_APP_NAME_LENGTH: 0x670a,

422

GEN_AES_KEY_FAILED: 0x5419,

423

INTERNAL_CRYPTO_OPERATION_FAILED: 0x541a,

424

INTERNAL_COMPUTE_AES_CMAC_FAILED: 0x541b,

425

ENCRYPT_APP_STORAGE_FAILED: 0x541c,

426

INVALID_BACKUP_STATE: 0x6642,

427

PIN_NOT_SET: 0x5502,

428

INVALID_BACKUP_LENGTH: 0x6733,

429

INVALID_RESTORE_STATE: 0x6643,

430

INVALID_CHUNK_LENGTH: 0x6734,

431

INVALID_BACKUP_HEADER: 0x684a,

432

TRUSTCHAIN_WRONG_SEED: 0xb007

433

};

434

435

/**

436

* Get alternative status message for status code

437

* @param code - Status code number

438

* @returns Human-readable status message or undefined

439

*/

440

function getAltStatusMessage(code: number): string | undefined | null {

441

switch (code) {

442

case 0x6700:

443

return "Incorrect length";

444

case 0x6800:

445

return "Missing critical parameter";

446

case 0x6982:

447

return "Security not satisfied (dongle locked or have invalid access rights)";

448

case 0x6985:

449

return "Condition of use not satisfied (denied by the user?)";

450

case 0x6a80:

451

return "Invalid data received";

452

case 0x6b00:

453

return "Invalid parameter received";

454

case 0x5515:

455

return "Locked device";

456

default:

457

if (0x6f00 <= code && code <= 0x6fff) {

458

return "Internal error, please report";

459

}

460

return undefined;

461

}

462

}

463

```

464

465

## Usage Examples

466

467

### Device Permission and Connection

468

469

```typescript

470

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

471

472

// Check browser support

473

if (await TransportWebUSB.isSupported()) {

474

try {

475

// Request device permission (always shows dialog)

476

const transport = await TransportWebUSB.request();

477

478

// Or connect silently if already authorized

479

const transport2 = await TransportWebUSB.openConnected();

480

481

if (transport2) {

482

console.log("Connected to:", transport2.deviceModel?.productName);

483

}

484

} catch (error) {

485

if (error instanceof TransportOpenUserCancelled) {

486

console.log("User cancelled device selection");

487

}

488

}

489

}

490

```

491

492

### Device Listening

493

494

```typescript

495

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

496

497

// Listen for device events (must be called on user interaction)

498

const subscription = TransportWebUSB.listen({

499

next: (event) => {

500

if (event.type === "add") {

501

console.log("Device connected:", event.deviceModel?.productName);

502

// Open transport with the detected device

503

TransportWebUSB.open(event.descriptor).then(transport => {

504

// Use transport...

505

});

506

}

507

},

508

error: (error) => {

509

console.error("Device listening error:", error);

510

},

511

complete: () => {

512

console.log("Device listening completed");

513

}

514

});

515

516

// Later, stop listening

517

subscription.unsubscribe();

518

```

519

520

### APDU Exchange

521

522

```typescript

523

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

524

525

const transport = await TransportWebUSB.create();

526

527

try {

528

// Low-level APDU exchange

529

const getAppNameAPDU = Buffer.from("E0C4000000", "hex");

530

const response = await transport.exchange(getAppNameAPDU);

531

532

// High-level send method with status handling

533

const appInfo = await transport.send(0xE0, 0xC4, 0x00, 0x00);

534

535

console.log("App info:", appInfo.toString("hex"));

536

} finally {

537

await transport.close();

538

}

539

```

540

541

### Error Handling

542

543

```typescript

544

import TransportWebUSB, {

545

TransportWebUSBGestureRequired,

546

TransportInterfaceNotAvailable,

547

DisconnectedDeviceDuringOperation

548

} from "@ledgerhq/hw-transport-webusb";

549

550

try {

551

const transport = await TransportWebUSB.create();

552

553

// Listen for disconnect events

554

transport.on("disconnect", (error) => {

555

console.log("Device disconnected:", error.message);

556

});

557

558

const response = await transport.exchange(apduBuffer);

559

} catch (error) {

560

if (error instanceof TransportWebUSBGestureRequired) {

561

console.log("Please trigger this action from a user click");

562

} else if (error instanceof TransportInterfaceNotAvailable) {

563

console.log("Device interface not available. Please upgrade firmware.");

564

} else if (error instanceof DisconnectedDeviceDuringOperation) {

565

console.log("Device was disconnected during operation");

566

}

567

}

568

```