npm package: @particle/esim-tooling
Description: TypeScript library for eSIM profile provisioning (SGP.22)
Author: Julien Vanier
Latest version: 1.1.2

# @particle/esim-tooling

TypeScript library for eSIM profile provisioning (SGP.22). Manages eSIM profiles on
devices with an eUICC — list, enable, disable, delete, and install profiles.

## Installation

```
npm install @particle/esim-tooling
```

## Quick Start

```typescript
import { EsimLpa } from '@particle/esim-tooling';
import type { DeviceAdapter, ServerAdapter } from '@particle/esim-tooling';

// Create adapters for your environment
const device: DeviceAdapter = createMyDeviceAdapter();
const server: ServerAdapter = createMyServerAdapter();

const lpa = new EsimLpa({ device, server });

// List profiles
const profiles = await lpa.listProfiles();
for (const p of profiles) {
  console.log(`${p.iccid} ${p.profileName} [${p.state === 1 ? 'enabled' : 'disabled'}]`);
}

// Enable a profile
await lpa.enableProfile('89012345678901234567');

// Disable a profile
await lpa.disableProfile('89012345678901234567');

// Delete a profile (must be disabled first)
await lpa.deleteProfile('89012345678901234567');

// Install a new profile
const result = await lpa.installProfile({
  activationCode: '1$smdp.example.com$ACTIVATION-TOKEN',
  onProgress: (p) => console.log(p.step),
});
console.log(`Installed: ${result.iccid}`);

// Process pending notifications
const notifResult = await lpa.processNotifications();
console.log(`Sent ${notifResult.sent}/${notifResult.total} notifications`);
```

## Adapters

This library does not directly communicate with devices or servers. You provide
two adapters that handle I/O.

### DeviceAdapter

Sends a single APDU to the eUICC and returns the response. The library handles
all higher-level protocol concerns (logical channels, STORE DATA chunking,
GET RESPONSE chaining, BPP segmentation).

```typescript
interface DeviceAdapter {
  sendApdu(apdu: Uint8Array): Promise<Uint8Array>;
}
```

**Example: Particle USB adapter**

```typescript
import { openDeviceById } from 'particle-usb';
import DeviceOSProtobuf from '@particle/device-os-protobuf';

const proto = {
  ManagerRequest: DeviceOSProtobuf.schema.system.esim.ManagerRequest,
  ManagerResponse: DeviceOSProtobuf.schema.system.esim.ManagerResponse,
};

function createParticleUsbAdapter(device): DeviceAdapter {
  return {
    async sendApdu(apdu: Uint8Array): Promise<Uint8Array> {
      // Encode APDU as protobuf ManagerRequest
      const request = proto.ManagerRequest.create({ apdu: { data: apdu } });
      const requestBytes = proto.ManagerRequest.encode(request).finish();
      const response = await device.sendControlRequest(10, Buffer.from(requestBytes));
      // Decode protobuf ManagerResponse, return APDU response
      const managerResponse = proto.ManagerResponse.decode(response.data);
      return new Uint8Array(managerResponse.apdu.data);
    }
  };
}
```

### ServerAdapter

Forwards ES9+ ASN.1 DER requests to SM-DP+ servers. SM-DP+ servers use TLS certificates
signed by the GSMA RSP2 Root CI, which is not in standard system CA stores.
The adapter owns the entire HTTP lifecycle — URL construction, headers, TLS.

```typescript
interface ServerAdapter {
  sendRsp(smdpAddress: string, endpoint: string, requestData: Uint8Array): Promise<ServerResponse>;
}

interface ServerResponse {
  statusCode: number;
  responseData?: Uint8Array;
}
```

**Example: direct adapter**

```typescript
const server: ServerAdapter = {
  async sendRsp(smdpAddress, endpoint, requestData) {
    const response = await fetch(`https://${smdpAddress}/gsma/rsp2/asn1`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-gsma-rsp-asn1',
        'User-Agent': 'gsma-rsp-lpad',
        'X-Admin-Protocol': 'gsma/rsp/v2.1.0',
      },
      body: requestData,
    });
    const statusCode = response.status;
    if (statusCode === 204) return { statusCode };
    const responseData = new Uint8Array(await response.arrayBuffer());
    return { statusCode, responseData };
  }
};
```

### Logging

The library provides utilities for logging APDU and ES9+ traffic, useful for debugging adapter implementations.

**LoggingDeviceAdapter** wraps any `DeviceAdapter` to log all APDU traffic:

```typescript
import { LoggingDeviceAdapter } from '@particle/esim-tooling';

// Wrap your adapter with default console logging
const loggingAdapter = new LoggingDeviceAdapter(rawAdapter);

// Or provide a custom logger
const loggingAdapter = new LoggingDeviceAdapter(rawAdapter, (event) => {
  // event.index: sequential APDU number (1-based)
  // event.direction: 'request' | 'response'
  // event.formatted: human-readable string
  // event.raw: Uint8Array of raw bytes
  // event.isError: true if response SW1 is not 0x90 or 0x61
  myLogger.debug(`APDU ${event.direction}: ${event.formatted}`);
});

const lpa = new EsimLpa({ device: loggingAdapter, server });
```

**LoggingServerAdapter** wraps any `ServerAdapter` to log all ES9+ traffic:

```typescript
import { LoggingServerAdapter } from '@particle/esim-tooling';

// Wrap your adapter with default console logging
const loggingAdapter = new LoggingServerAdapter(rawAdapter);

// Or provide a custom logger
const loggingAdapter = new LoggingServerAdapter(rawAdapter, (event) => {
  // event.index: sequential request number (1-based)
  // event.direction: 'request' | 'response'
  // event.formatted: human-readable string
  // event.endpoint: ES9+ endpoint name
  // event.smdpAddress: SM-DP+ server address
  // event.statusCode: HTTP status (response only)
  // event.raw: Uint8Array of raw bytes
  // event.isError: true if HTTP status >= 400
  myLogger.debug(`ES9+ ${event.direction}: ${event.formatted}`);
});

const lpa = new EsimLpa({ device, server: loggingAdapter });
```

**Standalone formatting functions** for custom logging:

```typescript
import {
  formatApduRequest, formatApduResponse, isApduError,
  formatServerRequest, formatServerResponse, isServerError,
} from '@particle/esim-tooling';

// APDU formatting
formatApduRequest(apdu);      // "ch1 STORE_DATA(255) MORE blk=0 (81e21100 ff ...)"
formatApduResponse(response); // "OK (9000)" or "MORE_DATA(16) (6110)"
isApduError(response);        // true if SW1 not 0x90 or 0x61

// ES9+ formatting
formatServerRequest('initiateAuthentication', 'smdp.example.com', data); // "initiateAuthentication smdp.example.com (123 bytes)"
formatServerResponse(200, data); // "HTTP 200 (456 bytes)"
isServerError(statusCode);       // true if status >= 400
```

## API Reference

### `EsimLpa`

| Method | Description |
|--------|-------------|
| `getEid()` | Get the 32-character hex EID |
| `listProfiles()` | List all profiles on the eUICC |
| `enableProfile(iccid)` | Enable a disabled profile |
| `disableProfile(iccid)` | Disable the active profile |
| `deleteProfile(iccid)` | Delete a disabled profile |
| `installProfile(options)` | Download and install a profile from SM-DP+ |
| `processNotifications()` | Send all pending notifications to SM-DP+ servers |

### Profile States

| Value | State |
|-------|-------|
| 0 | Disabled |
| 1 | Enabled |

### Notification Events

| Value | Event |
|-------|-------|
| 0x80 | Install |
| 0x40 | Enable |
| 0x20 | Disable |
| 0x10 | Delete |

## Error Handling

```typescript
import { Es10Error, Es9PlusError, DeviceError } from '@particle/esim-tooling';

try {
  await lpa.enableProfile(iccid);
} catch (error) {
  if (error instanceof Es10Error) {
    // eUICC returned an error (e.g., profile not found, policy violation)
    console.log('eUICC error:', error.resultCode, error.message);
  } else if (error instanceof Es9PlusError) {
    // SM-DP+ server error
    console.log('Server error:', error.statusCode, error.serverStatus);
  } else if (error instanceof DeviceError) {
    // Device communication error
    console.log('Device error:', error.result);
  }
}
```

## License

Apache-2.0