@particle/esim-tooling
v1.1.1
Published
TypeScript library for eSIM profile provisioning (SGP.22)
Readme
@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-toolingQuick Start
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).
interface DeviceAdapter {
sendApdu(apdu: Uint8Array): Promise<Uint8Array>;
}Example: Particle USB adapter
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.
interface ServerAdapter {
sendRsp(smdpAddress: string, endpoint: string, requestData: Uint8Array): Promise<ServerResponse>;
}
interface ServerResponse {
statusCode: number;
responseData?: Uint8Array;
}Example: direct adapter
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:
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:
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:
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 >= 400API 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+ |
| listNotifications() | List pending notification metadata |
| 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
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
