@particle/esim-tooling 1.0.0 → 1.1.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 +68 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logging-adapter.d.ts +90 -0
- package/dist/logging-adapter.d.ts.map +1 -0
- package/dist/logging-adapter.js +123 -0
- package/dist/logging-adapter.js.map +1 -0
- package/dist/logging-format.d.ts +71 -0
- package/dist/logging-format.d.ts.map +1 -0
- package/dist/logging-format.js +194 -0
- package/dist/logging-format.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +12 -0
- package/src/logging-adapter.ts +193 -0
- package/src/logging-format.ts +204 -0
package/README.md
CHANGED
|
@@ -130,6 +130,74 @@ const server: ServerAdapter = {
|
|
|
130
130
|
};
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
### Logging
|
|
134
|
+
|
|
135
|
+
The library provides utilities for logging APDU and ES9+ traffic, useful for debugging adapter implementations.
|
|
136
|
+
|
|
137
|
+
**LoggingDeviceAdapter** wraps any `DeviceAdapter` to log all APDU traffic:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { LoggingDeviceAdapter } from '@particle/esim-tooling';
|
|
141
|
+
|
|
142
|
+
// Wrap your adapter with default console logging
|
|
143
|
+
const loggingAdapter = new LoggingDeviceAdapter(rawAdapter);
|
|
144
|
+
|
|
145
|
+
// Or provide a custom logger
|
|
146
|
+
const loggingAdapter = new LoggingDeviceAdapter(rawAdapter, (event) => {
|
|
147
|
+
// event.index: sequential APDU number (1-based)
|
|
148
|
+
// event.direction: 'request' | 'response'
|
|
149
|
+
// event.formatted: human-readable string
|
|
150
|
+
// event.raw: Uint8Array of raw bytes
|
|
151
|
+
// event.isError: true if response SW1 is not 0x90 or 0x61
|
|
152
|
+
myLogger.debug(`APDU ${event.direction}: ${event.formatted}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const lpa = new EsimLpa({ device: loggingAdapter, server });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**LoggingServerAdapter** wraps any `ServerAdapter` to log all ES9+ traffic:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { LoggingServerAdapter } from '@particle/esim-tooling';
|
|
162
|
+
|
|
163
|
+
// Wrap your adapter with default console logging
|
|
164
|
+
const loggingAdapter = new LoggingServerAdapter(rawAdapter);
|
|
165
|
+
|
|
166
|
+
// Or provide a custom logger
|
|
167
|
+
const loggingAdapter = new LoggingServerAdapter(rawAdapter, (event) => {
|
|
168
|
+
// event.index: sequential request number (1-based)
|
|
169
|
+
// event.direction: 'request' | 'response'
|
|
170
|
+
// event.formatted: human-readable string
|
|
171
|
+
// event.endpoint: ES9+ endpoint name
|
|
172
|
+
// event.smdpAddress: SM-DP+ server address
|
|
173
|
+
// event.statusCode: HTTP status (response only)
|
|
174
|
+
// event.raw: Uint8Array of raw bytes
|
|
175
|
+
// event.isError: true if HTTP status >= 400
|
|
176
|
+
myLogger.debug(`ES9+ ${event.direction}: ${event.formatted}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const lpa = new EsimLpa({ device, server: loggingAdapter });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Standalone formatting functions** for custom logging:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import {
|
|
186
|
+
formatApduRequest, formatApduResponse, isApduError,
|
|
187
|
+
formatServerRequest, formatServerResponse, isServerError,
|
|
188
|
+
} from '@particle/esim-tooling';
|
|
189
|
+
|
|
190
|
+
// APDU formatting
|
|
191
|
+
formatApduRequest(apdu); // "ch1 STORE_DATA(255) MORE blk=0 (81e21100 ff ...)"
|
|
192
|
+
formatApduResponse(response); // "OK (9000)" or "MORE_DATA(16) (6110)"
|
|
193
|
+
isApduError(response); // true if SW1 not 0x90 or 0x61
|
|
194
|
+
|
|
195
|
+
// ES9+ formatting
|
|
196
|
+
formatServerRequest('initiateAuthentication', 'smdp.example.com', data); // "initiateAuthentication smdp.example.com (123 bytes)"
|
|
197
|
+
formatServerResponse(200, data); // "HTTP 200 (456 bytes)"
|
|
198
|
+
isServerError(statusCode); // true if status >= 400
|
|
199
|
+
```
|
|
200
|
+
|
|
133
201
|
## API Reference
|
|
134
202
|
|
|
135
203
|
### `EsimLpa`
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,7 @@ export { ProfileState, ProfileClass, NotificationEvent, CancelSessionReason, Ena
|
|
|
3
3
|
export type { DeviceAdapter, ServerAdapter, ServerResponse, Profile, Notification, NotificationMetadata, NotificationWithResult, SendResult, ProcessNotificationsOptions, ProcessNotificationsResult, ProfileInstallationResult, ActivationCode, DownloadStep, DownloadProgress, DownloadResult, InstallProfileOptions, EsimLpaOptions, } from './types.js';
|
|
4
4
|
export { EsimError, DeviceError, Es10Error, Es9PlusError, TlvError, ActivationCodeError, } from './errors.js';
|
|
5
5
|
export { parseActivationCode, isValidActivationCode, formatActivationCode } from './activation-code.js';
|
|
6
|
+
export { formatApduRequest, formatApduResponse, isApduError, formatServerRequest, formatServerResponse, isServerError, } from './logging-format.js';
|
|
7
|
+
export { LoggingDeviceAdapter, LoggingServerAdapter } from './logging-adapter.js';
|
|
8
|
+
export type { ApduLogEvent, ServerLogEvent } from './logging-adapter.js';
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,OAAO,EACP,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,2BAA2B,EAC3B,0BAA0B,EAC1B,yBAAyB,EACzB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,OAAO,EACP,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,2BAA2B,EAC3B,0BAA0B,EAC1B,yBAAyB,EACzB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAExG,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,mBAAmB,EACnB,oBAAoB,EACpB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAClF,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,6 @@ export { EsimLpa } from './lpa.js';
|
|
|
2
2
|
export { ProfileState, ProfileClass, NotificationEvent, CancelSessionReason, EnableProfileResult, DisableProfileResult, DeleteProfileResult, BppInstallErrorCode, } from './types.js';
|
|
3
3
|
export { EsimError, DeviceError, Es10Error, Es9PlusError, TlvError, ActivationCodeError, } from './errors.js';
|
|
4
4
|
export { parseActivationCode, isValidActivationCode, formatActivationCode } from './activation-code.js';
|
|
5
|
+
export { formatApduRequest, formatApduResponse, isApduError, formatServerRequest, formatServerResponse, isServerError, } from './logging-format.js';
|
|
6
|
+
export { LoggingDeviceAdapter, LoggingServerAdapter } from './logging-adapter.js';
|
|
5
7
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAsBpB,OAAO,EACL,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAsBpB,OAAO,EACL,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAExG,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,mBAAmB,EACnB,oBAAoB,EACpB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional logging wrappers for DeviceAdapter and ServerAdapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps adapters to log traffic using the decorator pattern.
|
|
5
|
+
* The underlying adapters are unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import type { DeviceAdapter, ServerAdapter, ServerResponse } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Event emitted for each APDU request or response.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApduLogEvent {
|
|
12
|
+
/** Sequential APDU index (1-based) */
|
|
13
|
+
index: number;
|
|
14
|
+
/** Whether this is a request or response */
|
|
15
|
+
direction: 'request' | 'response';
|
|
16
|
+
/** Human-readable formatted string */
|
|
17
|
+
formatted: string;
|
|
18
|
+
/** Raw APDU bytes */
|
|
19
|
+
raw: Uint8Array;
|
|
20
|
+
/** True if response SW1 is not 0x90 or 0x61 */
|
|
21
|
+
isError: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A DeviceAdapter wrapper that logs all APDU traffic.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { LoggingDeviceAdapter } from '@particle/esim-tooling';
|
|
29
|
+
*
|
|
30
|
+
* // Use default console logging
|
|
31
|
+
* const adapter = new LoggingDeviceAdapter(rawAdapter);
|
|
32
|
+
*
|
|
33
|
+
* // Or provide a custom logger
|
|
34
|
+
* const adapter = new LoggingDeviceAdapter(rawAdapter, (event) => {
|
|
35
|
+
* myLogger.debug(`APDU ${event.direction}: ${event.formatted}`);
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class LoggingDeviceAdapter implements DeviceAdapter {
|
|
40
|
+
private inner;
|
|
41
|
+
private logger;
|
|
42
|
+
private apduCount;
|
|
43
|
+
constructor(inner: DeviceAdapter, logger?: (event: ApduLogEvent) => void);
|
|
44
|
+
sendApdu(apdu: Uint8Array): Promise<Uint8Array>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Event emitted for each ES9+ server request or response.
|
|
48
|
+
*/
|
|
49
|
+
export interface ServerLogEvent {
|
|
50
|
+
/** Sequential request index (1-based) */
|
|
51
|
+
index: number;
|
|
52
|
+
/** Whether this is a request or response */
|
|
53
|
+
direction: 'request' | 'response';
|
|
54
|
+
/** Human-readable formatted string */
|
|
55
|
+
formatted: string;
|
|
56
|
+
/** ES9+ endpoint name */
|
|
57
|
+
endpoint: string;
|
|
58
|
+
/** SM-DP+ server address */
|
|
59
|
+
smdpAddress: string;
|
|
60
|
+
/** HTTP status code (response only) */
|
|
61
|
+
statusCode?: number;
|
|
62
|
+
/** Raw request/response bytes */
|
|
63
|
+
raw?: Uint8Array;
|
|
64
|
+
/** True if HTTP status >= 400 */
|
|
65
|
+
isError: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A ServerAdapter wrapper that logs all ES9+ traffic.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { LoggingServerAdapter } from '@particle/esim-tooling';
|
|
73
|
+
*
|
|
74
|
+
* // Use default console logging
|
|
75
|
+
* const adapter = new LoggingServerAdapter(rawAdapter);
|
|
76
|
+
*
|
|
77
|
+
* // Or provide a custom logger
|
|
78
|
+
* const adapter = new LoggingServerAdapter(rawAdapter, (event) => {
|
|
79
|
+
* myLogger.debug(`ES9+ ${event.direction}: ${event.formatted}`);
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare class LoggingServerAdapter implements ServerAdapter {
|
|
84
|
+
private inner;
|
|
85
|
+
private logger;
|
|
86
|
+
private requestCount;
|
|
87
|
+
constructor(inner: ServerAdapter, logger?: (event: ServerLogEvent) => void);
|
|
88
|
+
sendRsp(smdpAddress: string, endpoint: string, requestData: Uint8Array): Promise<ServerResponse>;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=logging-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging-adapter.d.ts","sourceRoot":"","sources":["../src/logging-adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAc/E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC;IAClC,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,GAAG,EAAE,UAAU,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;CAClB;AAUD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,oBAAqB,YAAW,aAAa;IACxD,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,SAAS,CAAK;gBAEV,KAAK,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI;IAKlE,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;CA2BtD;AAMD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC;IAClC,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;CAClB;AAUD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,oBAAqB,YAAW,aAAa;IACxD,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,YAAY,CAAK;gBAEb,KAAK,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI;IAKpE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC;CAgCvG"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional logging wrappers for DeviceAdapter and ServerAdapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps adapters to log traffic using the decorator pattern.
|
|
5
|
+
* The underlying adapters are unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import { formatApduRequest, formatApduResponse, isApduError, formatServerRequest, formatServerResponse, isServerError, } from './logging-format.js';
|
|
8
|
+
/**
|
|
9
|
+
* Default APDU logger that writes to console.
|
|
10
|
+
*/
|
|
11
|
+
function defaultApduLogger(event) {
|
|
12
|
+
const prefix = event.direction === 'request' ? '>>' : event.isError ? '!!' : '<<';
|
|
13
|
+
console.log(` APDU[${event.index}] ${prefix} ${event.formatted}`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A DeviceAdapter wrapper that logs all APDU traffic.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { LoggingDeviceAdapter } from '@particle/esim-tooling';
|
|
21
|
+
*
|
|
22
|
+
* // Use default console logging
|
|
23
|
+
* const adapter = new LoggingDeviceAdapter(rawAdapter);
|
|
24
|
+
*
|
|
25
|
+
* // Or provide a custom logger
|
|
26
|
+
* const adapter = new LoggingDeviceAdapter(rawAdapter, (event) => {
|
|
27
|
+
* myLogger.debug(`APDU ${event.direction}: ${event.formatted}`);
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class LoggingDeviceAdapter {
|
|
32
|
+
inner;
|
|
33
|
+
logger;
|
|
34
|
+
apduCount = 0;
|
|
35
|
+
constructor(inner, logger) {
|
|
36
|
+
this.inner = inner;
|
|
37
|
+
this.logger = logger ?? defaultApduLogger;
|
|
38
|
+
}
|
|
39
|
+
async sendApdu(apdu) {
|
|
40
|
+
this.apduCount++;
|
|
41
|
+
const index = this.apduCount;
|
|
42
|
+
// Log request
|
|
43
|
+
this.logger({
|
|
44
|
+
index,
|
|
45
|
+
direction: 'request',
|
|
46
|
+
formatted: formatApduRequest(apdu),
|
|
47
|
+
raw: apdu,
|
|
48
|
+
isError: false,
|
|
49
|
+
});
|
|
50
|
+
// Send to inner adapter
|
|
51
|
+
const resp = await this.inner.sendApdu(apdu);
|
|
52
|
+
// Log response
|
|
53
|
+
this.logger({
|
|
54
|
+
index,
|
|
55
|
+
direction: 'response',
|
|
56
|
+
formatted: formatApduResponse(resp),
|
|
57
|
+
raw: resp,
|
|
58
|
+
isError: isApduError(resp),
|
|
59
|
+
});
|
|
60
|
+
return resp;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Default server logger that writes to console.
|
|
65
|
+
*/
|
|
66
|
+
function defaultServerLogger(event) {
|
|
67
|
+
const prefix = event.direction === 'request' ? '>>' : event.isError ? '!!' : '<<';
|
|
68
|
+
console.log(` ES9+[${event.index}] ${prefix} ${event.formatted}`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A ServerAdapter wrapper that logs all ES9+ traffic.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import { LoggingServerAdapter } from '@particle/esim-tooling';
|
|
76
|
+
*
|
|
77
|
+
* // Use default console logging
|
|
78
|
+
* const adapter = new LoggingServerAdapter(rawAdapter);
|
|
79
|
+
*
|
|
80
|
+
* // Or provide a custom logger
|
|
81
|
+
* const adapter = new LoggingServerAdapter(rawAdapter, (event) => {
|
|
82
|
+
* myLogger.debug(`ES9+ ${event.direction}: ${event.formatted}`);
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export class LoggingServerAdapter {
|
|
87
|
+
inner;
|
|
88
|
+
logger;
|
|
89
|
+
requestCount = 0;
|
|
90
|
+
constructor(inner, logger) {
|
|
91
|
+
this.inner = inner;
|
|
92
|
+
this.logger = logger ?? defaultServerLogger;
|
|
93
|
+
}
|
|
94
|
+
async sendRsp(smdpAddress, endpoint, requestData) {
|
|
95
|
+
this.requestCount++;
|
|
96
|
+
const index = this.requestCount;
|
|
97
|
+
// Log request
|
|
98
|
+
this.logger({
|
|
99
|
+
index,
|
|
100
|
+
direction: 'request',
|
|
101
|
+
formatted: formatServerRequest(endpoint, smdpAddress, requestData),
|
|
102
|
+
endpoint,
|
|
103
|
+
smdpAddress,
|
|
104
|
+
raw: requestData,
|
|
105
|
+
isError: false,
|
|
106
|
+
});
|
|
107
|
+
// Send to inner adapter
|
|
108
|
+
const response = await this.inner.sendRsp(smdpAddress, endpoint, requestData);
|
|
109
|
+
// Log response
|
|
110
|
+
this.logger({
|
|
111
|
+
index,
|
|
112
|
+
direction: 'response',
|
|
113
|
+
formatted: formatServerResponse(response.statusCode, response.responseData),
|
|
114
|
+
endpoint,
|
|
115
|
+
smdpAddress,
|
|
116
|
+
statusCode: response.statusCode,
|
|
117
|
+
raw: response.responseData,
|
|
118
|
+
isError: isServerError(response.statusCode),
|
|
119
|
+
});
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=logging-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging-adapter.js","sourceRoot":"","sources":["../src/logging-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for logging and debugging APDU and ES9+ traffic.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Format an APDU request for logging.
|
|
6
|
+
*
|
|
7
|
+
* Returns a human-readable string with:
|
|
8
|
+
* - Logical channel number
|
|
9
|
+
* - INS name (MANAGE_CHANNEL, SELECT, STORE_DATA, GET_RESPONSE)
|
|
10
|
+
* - P1/P2 details (OPEN/CLOSE for MANAGE_CHANNEL, LAST/MORE for STORE_DATA)
|
|
11
|
+
* - Hex dump with spaces separating header, Lc, data, and Le
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* formatApduRequest(new Uint8Array([0x00, 0x70, 0x00, 0x00, 0x01]))
|
|
16
|
+
* // => "ch0 MANAGE_CHANNEL OPEN (00700000 01)"
|
|
17
|
+
*
|
|
18
|
+
* formatApduRequest(new Uint8Array([0x81, 0xe2, 0x91, 0x00, 0x05, ...data]))
|
|
19
|
+
* // => "ch1 STORE_DATA(5) LAST blk=0 (81e29100 05 ...)"
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatApduRequest(apdu: Uint8Array): string;
|
|
23
|
+
/**
|
|
24
|
+
* Format an APDU response for logging.
|
|
25
|
+
*
|
|
26
|
+
* Returns a human-readable string with:
|
|
27
|
+
* - Status word name (OK, FILE_NOT_FOUND, MORE_DATA, etc.)
|
|
28
|
+
* - Full hex dump including data and SW1 SW2
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* formatApduResponse(new Uint8Array([0x90, 0x00]))
|
|
33
|
+
* // => "OK (9000)"
|
|
34
|
+
*
|
|
35
|
+
* formatApduResponse(new Uint8Array([0x01, 0x02, 0x03, 0x61, 0x10]))
|
|
36
|
+
* // => "MORE_DATA(16) (010203 6110)"
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatApduResponse(resp: Uint8Array): string;
|
|
40
|
+
/**
|
|
41
|
+
* Check if an APDU response indicates an error (SW1 not 0x90 or 0x61).
|
|
42
|
+
*/
|
|
43
|
+
export declare function isApduError(resp: Uint8Array): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Format an ES9+ server request for logging.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* formatServerRequest('initiateAuthentication', 'smdp.example.com', data)
|
|
50
|
+
* // => "initiateAuthentication smdp.example.com (123 bytes)"
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatServerRequest(endpoint: string, smdpAddress: string, data: Uint8Array): string;
|
|
54
|
+
/**
|
|
55
|
+
* Format an ES9+ server response for logging.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* formatServerResponse(200, data)
|
|
60
|
+
* // => "HTTP 200 (456 bytes)"
|
|
61
|
+
*
|
|
62
|
+
* formatServerResponse(204)
|
|
63
|
+
* // => "HTTP 204 (no content)"
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function formatServerResponse(statusCode: number, data?: Uint8Array): string;
|
|
67
|
+
/**
|
|
68
|
+
* Check if an HTTP status code indicates an error (>= 400).
|
|
69
|
+
*/
|
|
70
|
+
export declare function isServerError(statusCode: number): boolean;
|
|
71
|
+
//# sourceMappingURL=logging-format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging-format.d.ts","sourceRoot":"","sources":["../src/logging-format.ts"],"names":[],"mappings":"AAAA;;GAEG;AAwDH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAmD1D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAM3D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAIrD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAEnG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAKlF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEzD"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for logging and debugging APDU and ES9+ traffic.
|
|
3
|
+
*/
|
|
4
|
+
/** Convert bytes to hex string (internal helper) */
|
|
5
|
+
function toHex(data) {
|
|
6
|
+
return Array.from(data)
|
|
7
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
8
|
+
.join('');
|
|
9
|
+
}
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// APDU formatting
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Decode INS byte to human-readable name */
|
|
14
|
+
function insName(ins) {
|
|
15
|
+
switch (ins) {
|
|
16
|
+
case 0x70:
|
|
17
|
+
return 'MANAGE_CHANNEL';
|
|
18
|
+
case 0xa4:
|
|
19
|
+
return 'SELECT';
|
|
20
|
+
case 0xe2:
|
|
21
|
+
return 'STORE_DATA';
|
|
22
|
+
case 0xc0:
|
|
23
|
+
return 'GET_RESPONSE';
|
|
24
|
+
default:
|
|
25
|
+
return ins.toString(16).padStart(2, '0');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Decode common status words */
|
|
29
|
+
function swName(sw1, sw2) {
|
|
30
|
+
const sw = (sw1 << 8) | sw2;
|
|
31
|
+
switch (sw) {
|
|
32
|
+
case 0x9000:
|
|
33
|
+
return 'OK';
|
|
34
|
+
case 0x6a82:
|
|
35
|
+
return 'FILE_NOT_FOUND';
|
|
36
|
+
case 0x6a88:
|
|
37
|
+
return 'REFERENCED_DATA_NOT_FOUND';
|
|
38
|
+
case 0x6985:
|
|
39
|
+
return 'CONDITIONS_NOT_SATISFIED';
|
|
40
|
+
case 0x6982:
|
|
41
|
+
return 'SECURITY_STATUS_NOT_SATISFIED';
|
|
42
|
+
default:
|
|
43
|
+
if (sw1 === 0x61)
|
|
44
|
+
return `MORE_DATA(${sw2})`;
|
|
45
|
+
if (sw1 === 0x6c)
|
|
46
|
+
return `WRONG_LE(${sw2})`;
|
|
47
|
+
return `${sw1.toString(16).padStart(2, '0')}${sw2.toString(16).padStart(2, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Extract logical channel from CLA byte */
|
|
51
|
+
function claChannel(cla) {
|
|
52
|
+
if ((cla & 0x40) === 0)
|
|
53
|
+
return cla & 0x03;
|
|
54
|
+
return 4 + (cla & 0x0f);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Format an APDU request for logging.
|
|
58
|
+
*
|
|
59
|
+
* Returns a human-readable string with:
|
|
60
|
+
* - Logical channel number
|
|
61
|
+
* - INS name (MANAGE_CHANNEL, SELECT, STORE_DATA, GET_RESPONSE)
|
|
62
|
+
* - P1/P2 details (OPEN/CLOSE for MANAGE_CHANNEL, LAST/MORE for STORE_DATA)
|
|
63
|
+
* - Hex dump with spaces separating header, Lc, data, and Le
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* formatApduRequest(new Uint8Array([0x00, 0x70, 0x00, 0x00, 0x01]))
|
|
68
|
+
* // => "ch0 MANAGE_CHANNEL OPEN (00700000 01)"
|
|
69
|
+
*
|
|
70
|
+
* formatApduRequest(new Uint8Array([0x81, 0xe2, 0x91, 0x00, 0x05, ...data]))
|
|
71
|
+
* // => "ch1 STORE_DATA(5) LAST blk=0 (81e29100 05 ...)"
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function formatApduRequest(apdu) {
|
|
75
|
+
if (apdu.length < 4)
|
|
76
|
+
return toHex(apdu);
|
|
77
|
+
const cla = apdu[0];
|
|
78
|
+
const ins = apdu[1];
|
|
79
|
+
const p1 = apdu[2];
|
|
80
|
+
const p2 = apdu[3];
|
|
81
|
+
const channel = claChannel(cla);
|
|
82
|
+
let detail = `ch${channel} ${insName(ins)}`;
|
|
83
|
+
// Add context based on INS
|
|
84
|
+
switch (ins) {
|
|
85
|
+
case 0x70: // MANAGE CHANNEL
|
|
86
|
+
detail += p2 === 0x00 && p1 === 0x00 ? ' OPEN' : ` CLOSE(${p2})`;
|
|
87
|
+
break;
|
|
88
|
+
case 0xe2: // STORE DATA
|
|
89
|
+
// Lc is byte 4 - how many bytes we're sending
|
|
90
|
+
if (apdu.length >= 5) {
|
|
91
|
+
const lc = apdu[4];
|
|
92
|
+
detail += `(${lc})`;
|
|
93
|
+
}
|
|
94
|
+
detail += p1 === 0x91 ? ' LAST' : ' MORE';
|
|
95
|
+
detail += ` blk=${p2}`;
|
|
96
|
+
break;
|
|
97
|
+
case 0xc0: // GET RESPONSE
|
|
98
|
+
// Le is the last byte - how many bytes we're requesting
|
|
99
|
+
if (apdu.length >= 5) {
|
|
100
|
+
const le = apdu[4];
|
|
101
|
+
detail += `(${le})`;
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
// Format hex with spaces separating: header Lc data [Le]
|
|
108
|
+
const header = toHex(apdu.subarray(0, 4));
|
|
109
|
+
let hex = header;
|
|
110
|
+
if (apdu.length > 4) {
|
|
111
|
+
const lc = apdu[4];
|
|
112
|
+
hex += ' ' + toHex(apdu.subarray(4, 5)); // Lc
|
|
113
|
+
if (lc > 0 && apdu.length > 5) {
|
|
114
|
+
hex += ' ' + toHex(apdu.subarray(5, 5 + lc)); // Data
|
|
115
|
+
}
|
|
116
|
+
if (apdu.length > 5 + lc) {
|
|
117
|
+
hex += ' ' + toHex(apdu.subarray(5 + lc)); // Le
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return `${detail} (${hex})`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Format an APDU response for logging.
|
|
124
|
+
*
|
|
125
|
+
* Returns a human-readable string with:
|
|
126
|
+
* - Status word name (OK, FILE_NOT_FOUND, MORE_DATA, etc.)
|
|
127
|
+
* - Full hex dump including data and SW1 SW2
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* formatApduResponse(new Uint8Array([0x90, 0x00]))
|
|
132
|
+
* // => "OK (9000)"
|
|
133
|
+
*
|
|
134
|
+
* formatApduResponse(new Uint8Array([0x01, 0x02, 0x03, 0x61, 0x10]))
|
|
135
|
+
* // => "MORE_DATA(16) (010203 6110)"
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function formatApduResponse(resp) {
|
|
139
|
+
if (resp.length < 2)
|
|
140
|
+
return `invalid (${toHex(resp)})`;
|
|
141
|
+
const sw1 = resp[resp.length - 2];
|
|
142
|
+
const sw2 = resp[resp.length - 1];
|
|
143
|
+
const swStr = swName(sw1, sw2);
|
|
144
|
+
return `${swStr} (${toHex(resp)})`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if an APDU response indicates an error (SW1 not 0x90 or 0x61).
|
|
148
|
+
*/
|
|
149
|
+
export function isApduError(resp) {
|
|
150
|
+
if (resp.length < 2)
|
|
151
|
+
return true;
|
|
152
|
+
const sw1 = resp[resp.length - 2];
|
|
153
|
+
return sw1 !== 0x90 && sw1 !== 0x61;
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// ES9+ server formatting
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Format an ES9+ server request for logging.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* formatServerRequest('initiateAuthentication', 'smdp.example.com', data)
|
|
164
|
+
* // => "initiateAuthentication smdp.example.com (123 bytes)"
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function formatServerRequest(endpoint, smdpAddress, data) {
|
|
168
|
+
return `${endpoint} ${smdpAddress} (${data.length} bytes)`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Format an ES9+ server response for logging.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* formatServerResponse(200, data)
|
|
176
|
+
* // => "HTTP 200 (456 bytes)"
|
|
177
|
+
*
|
|
178
|
+
* formatServerResponse(204)
|
|
179
|
+
* // => "HTTP 204 (no content)"
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export function formatServerResponse(statusCode, data) {
|
|
183
|
+
if (!data || data.length === 0) {
|
|
184
|
+
return `HTTP ${statusCode} (no content)`;
|
|
185
|
+
}
|
|
186
|
+
return `HTTP ${statusCode} (${data.length} bytes)`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if an HTTP status code indicates an error (>= 400).
|
|
190
|
+
*/
|
|
191
|
+
export function isServerError(statusCode) {
|
|
192
|
+
return statusCode >= 400;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=logging-format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging-format.js","sourceRoot":"","sources":["../src/logging-format.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -41,3 +41,15 @@ export {
|
|
|
41
41
|
} from './errors.js';
|
|
42
42
|
|
|
43
43
|
export { parseActivationCode, isValidActivationCode, formatActivationCode } from './activation-code.js';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
formatApduRequest,
|
|
47
|
+
formatApduResponse,
|
|
48
|
+
isApduError,
|
|
49
|
+
formatServerRequest,
|
|
50
|
+
formatServerResponse,
|
|
51
|
+
isServerError,
|
|
52
|
+
} from './logging-format.js';
|
|
53
|
+
|
|
54
|
+
export { LoggingDeviceAdapter, LoggingServerAdapter } from './logging-adapter.js';
|
|
55
|
+
export type { ApduLogEvent, ServerLogEvent } from './logging-adapter.js';
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional logging wrappers for DeviceAdapter and ServerAdapter.
|
|
3
|
+
*
|
|
4
|
+
* Wraps adapters to log traffic using the decorator pattern.
|
|
5
|
+
* The underlying adapters are unchanged.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DeviceAdapter, ServerAdapter, ServerResponse } from './types.js';
|
|
9
|
+
import {
|
|
10
|
+
formatApduRequest,
|
|
11
|
+
formatApduResponse,
|
|
12
|
+
isApduError,
|
|
13
|
+
formatServerRequest,
|
|
14
|
+
formatServerResponse,
|
|
15
|
+
isServerError,
|
|
16
|
+
} from './logging-format.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Device adapter logging
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Event emitted for each APDU request or response.
|
|
24
|
+
*/
|
|
25
|
+
export interface ApduLogEvent {
|
|
26
|
+
/** Sequential APDU index (1-based) */
|
|
27
|
+
index: number;
|
|
28
|
+
/** Whether this is a request or response */
|
|
29
|
+
direction: 'request' | 'response';
|
|
30
|
+
/** Human-readable formatted string */
|
|
31
|
+
formatted: string;
|
|
32
|
+
/** Raw APDU bytes */
|
|
33
|
+
raw: Uint8Array;
|
|
34
|
+
/** True if response SW1 is not 0x90 or 0x61 */
|
|
35
|
+
isError: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default APDU logger that writes to console.
|
|
40
|
+
*/
|
|
41
|
+
function defaultApduLogger(event: ApduLogEvent): void {
|
|
42
|
+
const prefix = event.direction === 'request' ? '>>' : event.isError ? '!!' : '<<';
|
|
43
|
+
console.log(` APDU[${event.index}] ${prefix} ${event.formatted}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A DeviceAdapter wrapper that logs all APDU traffic.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import { LoggingDeviceAdapter } from '@particle/esim-tooling';
|
|
52
|
+
*
|
|
53
|
+
* // Use default console logging
|
|
54
|
+
* const adapter = new LoggingDeviceAdapter(rawAdapter);
|
|
55
|
+
*
|
|
56
|
+
* // Or provide a custom logger
|
|
57
|
+
* const adapter = new LoggingDeviceAdapter(rawAdapter, (event) => {
|
|
58
|
+
* myLogger.debug(`APDU ${event.direction}: ${event.formatted}`);
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export class LoggingDeviceAdapter implements DeviceAdapter {
|
|
63
|
+
private inner: DeviceAdapter;
|
|
64
|
+
private logger: (event: ApduLogEvent) => void;
|
|
65
|
+
private apduCount = 0;
|
|
66
|
+
|
|
67
|
+
constructor(inner: DeviceAdapter, logger?: (event: ApduLogEvent) => void) {
|
|
68
|
+
this.inner = inner;
|
|
69
|
+
this.logger = logger ?? defaultApduLogger;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async sendApdu(apdu: Uint8Array): Promise<Uint8Array> {
|
|
73
|
+
this.apduCount++;
|
|
74
|
+
const index = this.apduCount;
|
|
75
|
+
|
|
76
|
+
// Log request
|
|
77
|
+
this.logger({
|
|
78
|
+
index,
|
|
79
|
+
direction: 'request',
|
|
80
|
+
formatted: formatApduRequest(apdu),
|
|
81
|
+
raw: apdu,
|
|
82
|
+
isError: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Send to inner adapter
|
|
86
|
+
const resp = await this.inner.sendApdu(apdu);
|
|
87
|
+
|
|
88
|
+
// Log response
|
|
89
|
+
this.logger({
|
|
90
|
+
index,
|
|
91
|
+
direction: 'response',
|
|
92
|
+
formatted: formatApduResponse(resp),
|
|
93
|
+
raw: resp,
|
|
94
|
+
isError: isApduError(resp),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return resp;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Server adapter logging
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Event emitted for each ES9+ server request or response.
|
|
107
|
+
*/
|
|
108
|
+
export interface ServerLogEvent {
|
|
109
|
+
/** Sequential request index (1-based) */
|
|
110
|
+
index: number;
|
|
111
|
+
/** Whether this is a request or response */
|
|
112
|
+
direction: 'request' | 'response';
|
|
113
|
+
/** Human-readable formatted string */
|
|
114
|
+
formatted: string;
|
|
115
|
+
/** ES9+ endpoint name */
|
|
116
|
+
endpoint: string;
|
|
117
|
+
/** SM-DP+ server address */
|
|
118
|
+
smdpAddress: string;
|
|
119
|
+
/** HTTP status code (response only) */
|
|
120
|
+
statusCode?: number;
|
|
121
|
+
/** Raw request/response bytes */
|
|
122
|
+
raw?: Uint8Array;
|
|
123
|
+
/** True if HTTP status >= 400 */
|
|
124
|
+
isError: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Default server logger that writes to console.
|
|
129
|
+
*/
|
|
130
|
+
function defaultServerLogger(event: ServerLogEvent): void {
|
|
131
|
+
const prefix = event.direction === 'request' ? '>>' : event.isError ? '!!' : '<<';
|
|
132
|
+
console.log(` ES9+[${event.index}] ${prefix} ${event.formatted}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A ServerAdapter wrapper that logs all ES9+ traffic.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* import { LoggingServerAdapter } from '@particle/esim-tooling';
|
|
141
|
+
*
|
|
142
|
+
* // Use default console logging
|
|
143
|
+
* const adapter = new LoggingServerAdapter(rawAdapter);
|
|
144
|
+
*
|
|
145
|
+
* // Or provide a custom logger
|
|
146
|
+
* const adapter = new LoggingServerAdapter(rawAdapter, (event) => {
|
|
147
|
+
* myLogger.debug(`ES9+ ${event.direction}: ${event.formatted}`);
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export class LoggingServerAdapter implements ServerAdapter {
|
|
152
|
+
private inner: ServerAdapter;
|
|
153
|
+
private logger: (event: ServerLogEvent) => void;
|
|
154
|
+
private requestCount = 0;
|
|
155
|
+
|
|
156
|
+
constructor(inner: ServerAdapter, logger?: (event: ServerLogEvent) => void) {
|
|
157
|
+
this.inner = inner;
|
|
158
|
+
this.logger = logger ?? defaultServerLogger;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async sendRsp(smdpAddress: string, endpoint: string, requestData: Uint8Array): Promise<ServerResponse> {
|
|
162
|
+
this.requestCount++;
|
|
163
|
+
const index = this.requestCount;
|
|
164
|
+
|
|
165
|
+
// Log request
|
|
166
|
+
this.logger({
|
|
167
|
+
index,
|
|
168
|
+
direction: 'request',
|
|
169
|
+
formatted: formatServerRequest(endpoint, smdpAddress, requestData),
|
|
170
|
+
endpoint,
|
|
171
|
+
smdpAddress,
|
|
172
|
+
raw: requestData,
|
|
173
|
+
isError: false,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Send to inner adapter
|
|
177
|
+
const response = await this.inner.sendRsp(smdpAddress, endpoint, requestData);
|
|
178
|
+
|
|
179
|
+
// Log response
|
|
180
|
+
this.logger({
|
|
181
|
+
index,
|
|
182
|
+
direction: 'response',
|
|
183
|
+
formatted: formatServerResponse(response.statusCode, response.responseData),
|
|
184
|
+
endpoint,
|
|
185
|
+
smdpAddress,
|
|
186
|
+
statusCode: response.statusCode,
|
|
187
|
+
raw: response.responseData,
|
|
188
|
+
isError: isServerError(response.statusCode),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for logging and debugging APDU and ES9+ traffic.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Convert bytes to hex string (internal helper) */
|
|
6
|
+
function toHex(data: Uint8Array): string {
|
|
7
|
+
return Array.from(data)
|
|
8
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
9
|
+
.join('');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// APDU formatting
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Decode INS byte to human-readable name */
|
|
17
|
+
function insName(ins: number): string {
|
|
18
|
+
switch (ins) {
|
|
19
|
+
case 0x70:
|
|
20
|
+
return 'MANAGE_CHANNEL';
|
|
21
|
+
case 0xa4:
|
|
22
|
+
return 'SELECT';
|
|
23
|
+
case 0xe2:
|
|
24
|
+
return 'STORE_DATA';
|
|
25
|
+
case 0xc0:
|
|
26
|
+
return 'GET_RESPONSE';
|
|
27
|
+
default:
|
|
28
|
+
return ins.toString(16).padStart(2, '0');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Decode common status words */
|
|
33
|
+
function swName(sw1: number, sw2: number): string {
|
|
34
|
+
const sw = (sw1 << 8) | sw2;
|
|
35
|
+
switch (sw) {
|
|
36
|
+
case 0x9000:
|
|
37
|
+
return 'OK';
|
|
38
|
+
case 0x6a82:
|
|
39
|
+
return 'FILE_NOT_FOUND';
|
|
40
|
+
case 0x6a88:
|
|
41
|
+
return 'REFERENCED_DATA_NOT_FOUND';
|
|
42
|
+
case 0x6985:
|
|
43
|
+
return 'CONDITIONS_NOT_SATISFIED';
|
|
44
|
+
case 0x6982:
|
|
45
|
+
return 'SECURITY_STATUS_NOT_SATISFIED';
|
|
46
|
+
default:
|
|
47
|
+
if (sw1 === 0x61) return `MORE_DATA(${sw2})`;
|
|
48
|
+
if (sw1 === 0x6c) return `WRONG_LE(${sw2})`;
|
|
49
|
+
return `${sw1.toString(16).padStart(2, '0')}${sw2.toString(16).padStart(2, '0')}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Extract logical channel from CLA byte */
|
|
54
|
+
function claChannel(cla: number): number {
|
|
55
|
+
if ((cla & 0x40) === 0) return cla & 0x03;
|
|
56
|
+
return 4 + (cla & 0x0f);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format an APDU request for logging.
|
|
61
|
+
*
|
|
62
|
+
* Returns a human-readable string with:
|
|
63
|
+
* - Logical channel number
|
|
64
|
+
* - INS name (MANAGE_CHANNEL, SELECT, STORE_DATA, GET_RESPONSE)
|
|
65
|
+
* - P1/P2 details (OPEN/CLOSE for MANAGE_CHANNEL, LAST/MORE for STORE_DATA)
|
|
66
|
+
* - Hex dump with spaces separating header, Lc, data, and Le
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* formatApduRequest(new Uint8Array([0x00, 0x70, 0x00, 0x00, 0x01]))
|
|
71
|
+
* // => "ch0 MANAGE_CHANNEL OPEN (00700000 01)"
|
|
72
|
+
*
|
|
73
|
+
* formatApduRequest(new Uint8Array([0x81, 0xe2, 0x91, 0x00, 0x05, ...data]))
|
|
74
|
+
* // => "ch1 STORE_DATA(5) LAST blk=0 (81e29100 05 ...)"
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function formatApduRequest(apdu: Uint8Array): string {
|
|
78
|
+
if (apdu.length < 4) return toHex(apdu);
|
|
79
|
+
|
|
80
|
+
const cla = apdu[0];
|
|
81
|
+
const ins = apdu[1];
|
|
82
|
+
const p1 = apdu[2];
|
|
83
|
+
const p2 = apdu[3];
|
|
84
|
+
const channel = claChannel(cla);
|
|
85
|
+
|
|
86
|
+
let detail = `ch${channel} ${insName(ins)}`;
|
|
87
|
+
|
|
88
|
+
// Add context based on INS
|
|
89
|
+
switch (ins) {
|
|
90
|
+
case 0x70: // MANAGE CHANNEL
|
|
91
|
+
detail += p2 === 0x00 && p1 === 0x00 ? ' OPEN' : ` CLOSE(${p2})`;
|
|
92
|
+
break;
|
|
93
|
+
case 0xe2: // STORE DATA
|
|
94
|
+
// Lc is byte 4 - how many bytes we're sending
|
|
95
|
+
if (apdu.length >= 5) {
|
|
96
|
+
const lc = apdu[4];
|
|
97
|
+
detail += `(${lc})`;
|
|
98
|
+
}
|
|
99
|
+
detail += p1 === 0x91 ? ' LAST' : ' MORE';
|
|
100
|
+
detail += ` blk=${p2}`;
|
|
101
|
+
break;
|
|
102
|
+
case 0xc0: // GET RESPONSE
|
|
103
|
+
// Le is the last byte - how many bytes we're requesting
|
|
104
|
+
if (apdu.length >= 5) {
|
|
105
|
+
const le = apdu[4];
|
|
106
|
+
detail += `(${le})`;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Format hex with spaces separating: header Lc data [Le]
|
|
114
|
+
const header = toHex(apdu.subarray(0, 4));
|
|
115
|
+
let hex = header;
|
|
116
|
+
if (apdu.length > 4) {
|
|
117
|
+
const lc = apdu[4];
|
|
118
|
+
hex += ' ' + toHex(apdu.subarray(4, 5)); // Lc
|
|
119
|
+
if (lc > 0 && apdu.length > 5) {
|
|
120
|
+
hex += ' ' + toHex(apdu.subarray(5, 5 + lc)); // Data
|
|
121
|
+
}
|
|
122
|
+
if (apdu.length > 5 + lc) {
|
|
123
|
+
hex += ' ' + toHex(apdu.subarray(5 + lc)); // Le
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return `${detail} (${hex})`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format an APDU response for logging.
|
|
132
|
+
*
|
|
133
|
+
* Returns a human-readable string with:
|
|
134
|
+
* - Status word name (OK, FILE_NOT_FOUND, MORE_DATA, etc.)
|
|
135
|
+
* - Full hex dump including data and SW1 SW2
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* formatApduResponse(new Uint8Array([0x90, 0x00]))
|
|
140
|
+
* // => "OK (9000)"
|
|
141
|
+
*
|
|
142
|
+
* formatApduResponse(new Uint8Array([0x01, 0x02, 0x03, 0x61, 0x10]))
|
|
143
|
+
* // => "MORE_DATA(16) (010203 6110)"
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function formatApduResponse(resp: Uint8Array): string {
|
|
147
|
+
if (resp.length < 2) return `invalid (${toHex(resp)})`;
|
|
148
|
+
const sw1 = resp[resp.length - 2];
|
|
149
|
+
const sw2 = resp[resp.length - 1];
|
|
150
|
+
const swStr = swName(sw1, sw2);
|
|
151
|
+
return `${swStr} (${toHex(resp)})`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if an APDU response indicates an error (SW1 not 0x90 or 0x61).
|
|
156
|
+
*/
|
|
157
|
+
export function isApduError(resp: Uint8Array): boolean {
|
|
158
|
+
if (resp.length < 2) return true;
|
|
159
|
+
const sw1 = resp[resp.length - 2];
|
|
160
|
+
return sw1 !== 0x90 && sw1 !== 0x61;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// ES9+ server formatting
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format an ES9+ server request for logging.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* formatServerRequest('initiateAuthentication', 'smdp.example.com', data)
|
|
173
|
+
* // => "initiateAuthentication smdp.example.com (123 bytes)"
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function formatServerRequest(endpoint: string, smdpAddress: string, data: Uint8Array): string {
|
|
177
|
+
return `${endpoint} ${smdpAddress} (${data.length} bytes)`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Format an ES9+ server response for logging.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* formatServerResponse(200, data)
|
|
186
|
+
* // => "HTTP 200 (456 bytes)"
|
|
187
|
+
*
|
|
188
|
+
* formatServerResponse(204)
|
|
189
|
+
* // => "HTTP 204 (no content)"
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export function formatServerResponse(statusCode: number, data?: Uint8Array): string {
|
|
193
|
+
if (!data || data.length === 0) {
|
|
194
|
+
return `HTTP ${statusCode} (no content)`;
|
|
195
|
+
}
|
|
196
|
+
return `HTTP ${statusCode} (${data.length} bytes)`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if an HTTP status code indicates an error (>= 400).
|
|
201
|
+
*/
|
|
202
|
+
export function isServerError(statusCode: number): boolean {
|
|
203
|
+
return statusCode >= 400;
|
|
204
|
+
}
|