@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 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle/esim-tooling",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TypeScript library for eSIM profile provisioning (SGP.22)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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
+ }