@particle/esim-tooling 1.0.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.
Files changed (77) hide show
  1. package/README.md +187 -0
  2. package/dist/activation-code.d.ts +11 -0
  3. package/dist/activation-code.d.ts.map +1 -0
  4. package/dist/activation-code.js +56 -0
  5. package/dist/activation-code.js.map +1 -0
  6. package/dist/apdu.d.ts +73 -0
  7. package/dist/apdu.d.ts.map +1 -0
  8. package/dist/apdu.js +357 -0
  9. package/dist/apdu.js.map +1 -0
  10. package/dist/errors.d.ts +32 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +52 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/es10/es10b-notifications.d.ts +30 -0
  15. package/dist/es10/es10b-notifications.d.ts.map +1 -0
  16. package/dist/es10/es10b-notifications.js +294 -0
  17. package/dist/es10/es10b-notifications.js.map +1 -0
  18. package/dist/es10/es10b.d.ts +34 -0
  19. package/dist/es10/es10b.d.ts.map +1 -0
  20. package/dist/es10/es10b.js +108 -0
  21. package/dist/es10/es10b.js.map +1 -0
  22. package/dist/es10/es10c.d.ts +12 -0
  23. package/dist/es10/es10c.d.ts.map +1 -0
  24. package/dist/es10/es10c.js +133 -0
  25. package/dist/es10/es10c.js.map +1 -0
  26. package/dist/es10/iccid.d.ts +9 -0
  27. package/dist/es10/iccid.d.ts.map +1 -0
  28. package/dist/es10/iccid.js +31 -0
  29. package/dist/es10/iccid.js.map +1 -0
  30. package/dist/es10/index.d.ts +5 -0
  31. package/dist/es10/index.d.ts.map +1 -0
  32. package/dist/es10/index.js +4 -0
  33. package/dist/es10/index.js.map +1 -0
  34. package/dist/es10/tags.d.ts +55 -0
  35. package/dist/es10/tags.d.ts.map +1 -0
  36. package/dist/es10/tags.js +63 -0
  37. package/dist/es10/tags.js.map +1 -0
  38. package/dist/es9plus.d.ts +52 -0
  39. package/dist/es9plus.d.ts.map +1 -0
  40. package/dist/es9plus.js +227 -0
  41. package/dist/es9plus.js.map +1 -0
  42. package/dist/gsma-rsp2-root-ci1.d.ts +38 -0
  43. package/dist/gsma-rsp2-root-ci1.d.ts.map +1 -0
  44. package/dist/gsma-rsp2-root-ci1.js +52 -0
  45. package/dist/gsma-rsp2-root-ci1.js.map +1 -0
  46. package/dist/index.d.ts +6 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +5 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/lpa.d.ts +15 -0
  51. package/dist/lpa.d.ts.map +1 -0
  52. package/dist/lpa.js +283 -0
  53. package/dist/lpa.js.map +1 -0
  54. package/dist/tlv.d.ts +14 -0
  55. package/dist/tlv.d.ts.map +1 -0
  56. package/dist/tlv.js +132 -0
  57. package/dist/tlv.js.map +1 -0
  58. package/dist/types.d.ts +230 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +108 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +50 -0
  63. package/src/activation-code.ts +64 -0
  64. package/src/apdu.ts +419 -0
  65. package/src/errors.ts +69 -0
  66. package/src/es10/es10b-notifications.ts +331 -0
  67. package/src/es10/es10b.ts +163 -0
  68. package/src/es10/es10c.ts +168 -0
  69. package/src/es10/iccid.ts +32 -0
  70. package/src/es10/index.ts +42 -0
  71. package/src/es10/tags.ts +69 -0
  72. package/src/es9plus.ts +331 -0
  73. package/src/gsma-rsp2-root-ci1.ts +53 -0
  74. package/src/index.ts +43 -0
  75. package/src/lpa.ts +346 -0
  76. package/src/tlv.ts +137 -0
  77. package/src/types.ts +264 -0
@@ -0,0 +1,331 @@
1
+ import { Es10Error } from '../errors.js';
2
+ import { tlvEncode, tlvDecode, tlvFind, tlvFindAll, tlvConcat } from '../tlv.js';
3
+ import { decodeIccid } from './iccid.js';
4
+ import { NotificationEvent, DeleteNotificationStatus, type NotificationMetadata, type Notification, type ProfileInstallationResult } from '../types.js';
5
+ import * as Tags from './tags.js';
6
+ import type { TlvNode } from '../tlv.js';
7
+
8
+ /**
9
+ * Notification with raw PendingNotification bytes for sending to SM-DP+.
10
+ * Internal type used by processNotifications.
11
+ */
12
+ export interface NotificationWithRaw extends Notification {
13
+ /** Raw PendingNotification bytes (BF37 or 30) to send to SM-DP+ handleNotification */
14
+ raw: Uint8Array;
15
+ }
16
+
17
+ export function encodeListNotificationRequest(): Uint8Array {
18
+ return tlvEncode(Tags.TAG_LIST_NOTIFICATION_REQUEST, new Uint8Array(0));
19
+ }
20
+
21
+ export function decodeListNotificationResponse(data: Uint8Array): NotificationMetadata[] {
22
+ const root = tlvDecode(data);
23
+ if (root.tag !== Tags.TAG_LIST_NOTIFICATION_REQUEST) {
24
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
25
+ }
26
+
27
+ // Check for empty list or error
28
+ const metadataList = tlvFind(root, Tags.TAG_NOTIFICATION_METADATA_LIST);
29
+ if (!metadataList) {
30
+ // Could be an error response or empty
31
+ return [];
32
+ }
33
+
34
+ const result: NotificationMetadata[] = [];
35
+ const items = tlvFindAll(metadataList, Tags.TAG_NOTIFICATION_METADATA);
36
+ for (const item of items) {
37
+ const metadata = decodeNotificationMetadata(item);
38
+ if (metadata) result.push(metadata);
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function decodeNotificationMetadata(node: TlvNode): NotificationMetadata | null {
44
+ const seqNode = tlvFind(node, Tags.TAG_NOTIFICATION_SEQ_NUMBER);
45
+ const opNode = tlvFind(node, Tags.TAG_NOTIFICATION_OPERATION);
46
+ const addrNode = tlvFind(node, Tags.TAG_NOTIFICATION_ADDRESS);
47
+
48
+ if (!seqNode || !opNode || !addrNode) return null;
49
+
50
+ const seqNumber = decodeUnsignedInteger(seqNode.value);
51
+ const operation = decodeNotificationEvent(opNode.value);
52
+ const notificationAddress = new TextDecoder('utf-8').decode(addrNode.value);
53
+
54
+ const iccidNode = tlvFind(node, Tags.TAG_NOTIFICATION_ICCID);
55
+ const iccid = iccidNode ? decodeIccid(iccidNode.value) : undefined;
56
+
57
+ return { seqNumber, operation, notificationAddress, iccid };
58
+ }
59
+
60
+ export function encodeRetrieveNotificationsListRequest(seqNumber?: number): Uint8Array {
61
+ if (seqNumber === undefined) {
62
+ // No filter - retrieve all notifications
63
+ return tlvEncode(Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST_REQUEST, new Uint8Array(0));
64
+ }
65
+ const seqTlv = tlvEncode(Tags.TAG_NOTIFICATION_SEQ_NUMBER, encodeUnsignedInteger(seqNumber));
66
+ const criteria = tlvEncode(Tags.TAG_NOTIFICATION_SEARCH_CRITERIA, seqTlv);
67
+ return tlvEncode(Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST_REQUEST, criteria);
68
+ }
69
+
70
+ /**
71
+ * Decode RetrieveNotificationsList response returning raw bytes for a single notification.
72
+ * Used by processNotifications to forward to SM-DP+.
73
+ */
74
+ export function decodeRetrieveNotificationsListResponse(data: Uint8Array): Uint8Array {
75
+ const root = tlvDecode(data);
76
+ if (root.tag !== Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST_REQUEST) {
77
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
78
+ }
79
+
80
+ // Check for error response: BF2B { 02 <error-code> }
81
+ // notificationsListResultError is an INTEGER (tag 0x02)
82
+ const errorNode = tlvFind(root, 0x02);
83
+ if (errorNode) {
84
+ const errorCode = errorNode.value[0] ?? 127;
85
+ throw new Es10Error(errorCode, `RetrieveNotificationsList failed: error=${errorCode}`);
86
+ }
87
+
88
+ // Response structure: BF2B { A0 { BF37 or 30 } }
89
+ // The A0 is the notificationList wrapper (CHOICE alternative [0])
90
+ const notificationList = tlvFind(root, 0xa0);
91
+ const searchNode = notificationList ?? root;
92
+
93
+ // The pending notification is the raw value to forward to SM-DP+
94
+ // Could be BF37 (ProfileInstallationResult) or 30 (OtherSignedNotification)
95
+ const installResult = tlvFind(searchNode, Tags.TAG_PROFILE_INSTALLATION_RESULT);
96
+ if (installResult) {
97
+ return installResult.raw ?? installResult.value;
98
+ }
99
+ const otherNotif = tlvFind(searchNode, Tags.TAG_OTHER_SIGNED_NOTIFICATION);
100
+ if (otherNotif) {
101
+ return otherNotif.raw ?? otherNotif.value;
102
+ }
103
+ throw new Es10Error(-1, 'PendingNotification not found in response');
104
+ }
105
+
106
+ /**
107
+ * Decode RetrieveNotificationsList response returning full Notification objects
108
+ * with raw bytes for sending to SM-DP+.
109
+ */
110
+ export function decodeRetrieveNotificationsListFull(data: Uint8Array): NotificationWithRaw[] {
111
+ const root = tlvDecode(data);
112
+ if (root.tag !== Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST_REQUEST) {
113
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
114
+ }
115
+
116
+ // Check for error response: BF2B { 02 <error-code> }
117
+ // notificationsListResultError is an INTEGER (tag 0x02)
118
+ const errorNode = tlvFind(root, 0x02);
119
+ if (errorNode) {
120
+ const errorCode = errorNode.value[0] ?? 127;
121
+ throw new Es10Error(errorCode, `RetrieveNotificationsList failed: error=${errorCode}`);
122
+ }
123
+
124
+ const result: NotificationWithRaw[] = [];
125
+
126
+ // Response structure: BF2B { A0 { BF37... 30... } }
127
+ // The A0 is the notificationList wrapper (CHOICE alternative [0])
128
+ const notificationList = tlvFind(root, 0xa0);
129
+ const searchNode = notificationList ?? root;
130
+
131
+ // Find all ProfileInstallationResult (BF37) nodes - install notifications
132
+ const installResults = tlvFindAll(searchNode, Tags.TAG_PROFILE_INSTALLATION_RESULT);
133
+ for (const node of installResults) {
134
+ const notif = decodeProfileInstallationResult(node);
135
+ if (notif) {
136
+ // Raw bytes are the entire PendingNotification (BF37 node)
137
+ const raw = node.raw ?? new Uint8Array(0);
138
+ result.push({ ...notif, raw });
139
+ }
140
+ }
141
+
142
+ // Find all OtherSignedNotification (30) nodes - enable/disable/delete notifications
143
+ const otherNotifs = tlvFindAll(searchNode, Tags.TAG_OTHER_SIGNED_NOTIFICATION);
144
+ for (const node of otherNotifs) {
145
+ const notif = decodeOtherSignedNotification(node);
146
+ if (notif) {
147
+ // Raw bytes are the entire PendingNotification (30 node)
148
+ const raw = node.raw ?? new Uint8Array(0);
149
+ result.push({ ...notif, raw });
150
+ }
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Decode ProfileInstallationResult (BF37) to extract metadata and success/failure.
158
+ */
159
+ function decodeProfileInstallationResult(node: TlvNode): Notification | null {
160
+ // ProfileInstallationResult contains ProfileInstallationResultData (BF27)
161
+ const resultData = tlvFind(node, Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA);
162
+ if (!resultData) return null;
163
+
164
+ // Extract NotificationMetadata (BF2F) from within
165
+ const metadataNode = tlvFind(resultData, Tags.TAG_NOTIFICATION_METADATA);
166
+ if (!metadataNode) return null;
167
+
168
+ const metadata = decodeNotificationMetadata(metadataNode);
169
+ if (!metadata) return null;
170
+
171
+ // The result structure can be:
172
+ // 1. A2 (finalResult wrapper) containing either:
173
+ // - A0 (successResult) with ICCID
174
+ // - A1 (errorResult) with error codes
175
+ // 2. A1 (errorResult) directly in BF27 (without A2 wrapper)
176
+ //
177
+ // Check for A2 first, then look inside it for A0 or A1
178
+ const finalResult = tlvFind(resultData, Tags.TAG_FINAL_RESULT);
179
+ if (finalResult) {
180
+ // Check if A2 contains A1 (error) - this takes precedence
181
+ const errorInFinal = tlvFind(finalResult, Tags.TAG_ERROR_RESULT);
182
+ if (errorInFinal) {
183
+ const subjectNode = tlvFind(errorInFinal, Tags.TAG_SUBJECT_CODE);
184
+ const reasonNode = tlvFind(errorInFinal, Tags.TAG_REASON_CODE);
185
+ return {
186
+ ...metadata,
187
+ success: false,
188
+ errorSubjectCode: subjectNode ? decodeUnsignedInteger(subjectNode.value) : undefined,
189
+ errorReasonCode: reasonNode ? decodeUnsignedInteger(reasonNode.value) : undefined,
190
+ };
191
+ }
192
+ // A2 without A1 inside = success, extract ICCID
193
+ const iccidNode = tlvFind(finalResult, Tags.TAG_NOTIFICATION_ICCID);
194
+ const iccid = iccidNode ? decodeIccid(iccidNode.value) : metadata.iccid;
195
+ return { ...metadata, iccid, success: true };
196
+ }
197
+
198
+ // Check for A1 directly in BF27 (without A2 wrapper)
199
+ const errorResult = tlvFind(resultData, Tags.TAG_ERROR_RESULT);
200
+ if (errorResult) {
201
+ const subjectNode = tlvFind(errorResult, Tags.TAG_SUBJECT_CODE);
202
+ const reasonNode = tlvFind(errorResult, Tags.TAG_REASON_CODE);
203
+ return {
204
+ ...metadata,
205
+ success: false,
206
+ errorSubjectCode: subjectNode ? decodeUnsignedInteger(subjectNode.value) : undefined,
207
+ errorReasonCode: reasonNode ? decodeUnsignedInteger(reasonNode.value) : undefined,
208
+ };
209
+ }
210
+
211
+ // No result info - assume success (shouldn't happen per spec)
212
+ return { ...metadata, success: true };
213
+ }
214
+
215
+ /**
216
+ * Decode OtherSignedNotification (SEQUENCE tag 30) for enable/disable/delete.
217
+ * These always represent completed operations (success=true).
218
+ */
219
+ function decodeOtherSignedNotification(node: TlvNode): Notification | null {
220
+ // OtherSignedNotification contains tbsOtherNotification which is NotificationMetadata
221
+ const metadataNode = tlvFind(node, Tags.TAG_NOTIFICATION_METADATA);
222
+ if (!metadataNode) return null;
223
+
224
+ const metadata = decodeNotificationMetadata(metadataNode);
225
+ if (!metadata) return null;
226
+
227
+ // Enable/disable/delete notifications are always for completed operations
228
+ return { ...metadata, success: true };
229
+ }
230
+
231
+ /**
232
+ * Decode ProfileInstallationResult (BF37) from BPP loading response.
233
+ * Returns success/failure status and ICCID or error codes.
234
+ */
235
+ export function decodeBppInstallationResult(data: Uint8Array): ProfileInstallationResult {
236
+ const root = tlvDecode(data);
237
+ if (root.tag !== Tags.TAG_PROFILE_INSTALLATION_RESULT) {
238
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}, expected BF37`);
239
+ }
240
+
241
+ // ProfileInstallationResult contains ProfileInstallationResultData (BF27)
242
+ const resultData = tlvFind(root, Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA);
243
+ if (!resultData) {
244
+ throw new Es10Error(-1, 'ProfileInstallationResultData (BF27) not found');
245
+ }
246
+
247
+ // The result structure can be:
248
+ // 1. A2 (finalResult wrapper) containing either:
249
+ // - A1 (errorResult) with error codes
250
+ // - 5A (ICCID) indicating success
251
+ // 2. A1 (errorResult) directly in BF27
252
+ //
253
+ // Check for A2 first, then look inside it for A1 or ICCID
254
+ const finalResult = tlvFind(resultData, Tags.TAG_FINAL_RESULT);
255
+ if (finalResult) {
256
+ // Check if A2 contains A1 (error) - this takes precedence
257
+ const errorInFinal = tlvFind(finalResult, Tags.TAG_ERROR_RESULT);
258
+ if (errorInFinal) {
259
+ const subjectNode = tlvFind(errorInFinal, Tags.TAG_SUBJECT_CODE);
260
+ const reasonNode = tlvFind(errorInFinal, Tags.TAG_REASON_CODE);
261
+ return {
262
+ success: false,
263
+ errorSubjectCode: subjectNode ? decodeUnsignedInteger(subjectNode.value) : undefined,
264
+ errorReasonCode: reasonNode ? decodeUnsignedInteger(reasonNode.value) : undefined,
265
+ };
266
+ }
267
+ // A2 without A1 inside = success, extract ICCID
268
+ const iccidNode = tlvFind(finalResult, Tags.TAG_NOTIFICATION_ICCID);
269
+ const iccid = iccidNode ? decodeIccid(iccidNode.value) : undefined;
270
+ return { success: true, iccid };
271
+ }
272
+
273
+ // Check for A1 directly in BF27 (without A2 wrapper)
274
+ const errorResult = tlvFind(resultData, Tags.TAG_ERROR_RESULT);
275
+ if (errorResult) {
276
+ const subjectNode = tlvFind(errorResult, Tags.TAG_SUBJECT_CODE);
277
+ const reasonNode = tlvFind(errorResult, Tags.TAG_REASON_CODE);
278
+ return {
279
+ success: false,
280
+ errorSubjectCode: subjectNode ? decodeUnsignedInteger(subjectNode.value) : undefined,
281
+ errorReasonCode: reasonNode ? decodeUnsignedInteger(reasonNode.value) : undefined,
282
+ };
283
+ }
284
+
285
+ // No explicit result - shouldn't happen per spec
286
+ return { success: true };
287
+ }
288
+
289
+ export function encodeRemoveNotificationRequest(seqNumber: number): Uint8Array {
290
+ const seqTlv = tlvEncode(Tags.TAG_NOTIFICATION_SEQ_NUMBER, encodeUnsignedInteger(seqNumber));
291
+ return tlvEncode(Tags.TAG_REMOVE_NOTIFICATION_REQUEST, seqTlv);
292
+ }
293
+
294
+ export function decodeRemoveNotificationResponse(data: Uint8Array): DeleteNotificationStatus {
295
+ const root = tlvDecode(data);
296
+ if (root.tag !== Tags.TAG_REMOVE_NOTIFICATION_REQUEST) {
297
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
298
+ }
299
+ const status = tlvFind(root, Tags.TAG_DELETE_NOTIFICATION_STATUS);
300
+ if (!status) {
301
+ throw new Es10Error(-1, 'DeleteNotificationStatus not found in response');
302
+ }
303
+ return status.value[0] as DeleteNotificationStatus;
304
+ }
305
+
306
+ function decodeNotificationEvent(value: Uint8Array): NotificationEvent {
307
+ // NotificationEvent is a BIT STRING — first byte is number of unused bits, rest is the bitmap
308
+ if (value.length < 2) return NotificationEvent.Install;
309
+ return value[1] as NotificationEvent;
310
+ }
311
+
312
+ function encodeUnsignedInteger(value: number): Uint8Array {
313
+ if (value < 0x80) return new Uint8Array([value]);
314
+ if (value < 0x100) return new Uint8Array([0x00, value]);
315
+ if (value < 0x8000) return new Uint8Array([(value >> 8) & 0xff, value & 0xff]);
316
+ if (value < 0x10000) return new Uint8Array([0x00, (value >> 8) & 0xff, value & 0xff]);
317
+ return new Uint8Array([
318
+ 0x00,
319
+ (value >> 16) & 0xff,
320
+ (value >> 8) & 0xff,
321
+ value & 0xff,
322
+ ]);
323
+ }
324
+
325
+ function decodeUnsignedInteger(value: Uint8Array): number {
326
+ let result = 0;
327
+ for (let i = 0; i < value.length; i++) {
328
+ result = (result << 8) | value[i];
329
+ }
330
+ return result;
331
+ }
@@ -0,0 +1,163 @@
1
+ import { Es10Error } from '../errors.js';
2
+ import { tlvEncode, tlvDecode, tlvFind, tlvConcat } from '../tlv.js';
3
+ import type { CancelSessionReason } from '../types.js';
4
+ import * as Tags from './tags.js';
5
+
6
+ export interface AuthenticateServerParams {
7
+ serverSigned1: Uint8Array;
8
+ serverSignature1: Uint8Array;
9
+ euiccCiPKIdToBeUsed: Uint8Array;
10
+ serverCertificate: Uint8Array;
11
+ matchingId?: string;
12
+ }
13
+
14
+ export interface PrepareDownloadParams {
15
+ smdpSigned2: Uint8Array;
16
+ smdpSignature2: Uint8Array;
17
+ smdpCertificate: Uint8Array;
18
+ hashCc?: Uint8Array;
19
+ }
20
+
21
+ export interface AuthenticateServerDecodedResponse {
22
+ raw: Uint8Array;
23
+ }
24
+
25
+ export interface PrepareDownloadDecodedResponse {
26
+ raw: Uint8Array;
27
+ }
28
+
29
+ export function encodeGetEuiccChallengeRequest(): Uint8Array {
30
+ return tlvEncode(Tags.TAG_GET_EUICC_CHALLENGE_REQUEST, new Uint8Array(0));
31
+ }
32
+
33
+ export function decodeGetEuiccChallengeResponse(data: Uint8Array): Uint8Array {
34
+ const root = tlvDecode(data);
35
+ if (root.tag !== Tags.TAG_GET_EUICC_CHALLENGE_REQUEST) {
36
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
37
+ }
38
+ const challenge = tlvFind(root, Tags.TAG_EUICC_CHALLENGE);
39
+ if (!challenge) {
40
+ throw new Es10Error(-1, 'EuiccChallenge not found in response');
41
+ }
42
+ return challenge.value;
43
+ }
44
+
45
+ export function encodeGetEuiccInfo1Request(): Uint8Array {
46
+ return tlvEncode(Tags.TAG_GET_EUICC_INFO1_REQUEST, new Uint8Array(0));
47
+ }
48
+
49
+ export function encodeAuthenticateServerRequest(params: AuthenticateServerParams): Uint8Array {
50
+ // Build ctxParams1 with matchingId and deviceInfo
51
+ const ctxParts: Uint8Array[] = [];
52
+
53
+ if (params.matchingId) {
54
+ ctxParts.push(tlvEncode(Tags.TAG_MATCHING_ID, new TextEncoder().encode(params.matchingId)));
55
+ }
56
+
57
+ // DeviceInfo with dummy TAC 0x88888888 and empty deviceCapabilities
58
+ // Per SGP.22, DeviceInfo ::= SEQUENCE { tac [0], deviceCapabilities [1] }
59
+ // Both fields are MANDATORY. deviceCapabilities can be empty but must be present.
60
+ const tac = tlvEncode(0x80, new Uint8Array([0x88, 0x88, 0x88, 0x88])); // [0] tac
61
+ const deviceCapabilities = tlvEncode(0xa1, new Uint8Array(0)); // [1] deviceCapabilities (empty SEQUENCE)
62
+ const deviceInfo = tlvEncode(Tags.TAG_DEVICE_INFO, tlvConcat(tac, deviceCapabilities));
63
+ ctxParts.push(deviceInfo);
64
+
65
+ const ctxParams1 = tlvEncode(Tags.TAG_CTX_PARAMS1, tlvConcat(...ctxParts));
66
+
67
+ // The request wraps: serverSigned1, serverSignature1, euiccCiPKIdToBeUsed, serverCertificate, ctxParams1
68
+ // serverSigned1, serverSignature1, euiccCiPKIdToBeUsed, serverCertificate are raw DER passed through
69
+ const content = tlvConcat(
70
+ params.serverSigned1,
71
+ params.serverSignature1,
72
+ params.euiccCiPKIdToBeUsed,
73
+ params.serverCertificate,
74
+ ctxParams1,
75
+ );
76
+
77
+ return tlvEncode(Tags.TAG_AUTHENTICATE_SERVER_REQUEST, content);
78
+ }
79
+
80
+ export function decodeAuthenticateServerResponse(data: Uint8Array): AuthenticateServerDecodedResponse {
81
+ const root = tlvDecode(data);
82
+ if (root.tag !== Tags.TAG_AUTHENTICATE_SERVER_REQUEST) {
83
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
84
+ }
85
+
86
+ // Check for error (A1 tag)
87
+ const errorNode = tlvFind(root, Tags.TAG_AUTH_SERVER_RESPONSE_ERROR);
88
+ if (errorNode) {
89
+ throw new Es10Error(-1, 'AuthenticateServer failed on eUICC');
90
+ }
91
+
92
+ // Success (A0 tag) — preserve raw bytes for passthrough to SM-DP+
93
+ const okNode = tlvFind(root, Tags.TAG_AUTH_SERVER_RESPONSE_OK);
94
+ if (!okNode) {
95
+ throw new Es10Error(-1, 'Unexpected AuthenticateServer response format');
96
+ }
97
+
98
+ return { raw: root.raw ?? data };
99
+ }
100
+
101
+ export function encodePrepareDownloadRequest(params: PrepareDownloadParams): Uint8Array {
102
+ // Field order per SGP.22: smdpSigned2 → smdpSignature2 → hashCc (optional) → smdpCertificate
103
+ const parts: Uint8Array[] = [
104
+ params.smdpSigned2,
105
+ params.smdpSignature2,
106
+ ];
107
+
108
+ if (params.hashCc) {
109
+ parts.push(tlvEncode(Tags.TAG_HASH_CC, params.hashCc));
110
+ }
111
+
112
+ parts.push(params.smdpCertificate);
113
+
114
+ return tlvEncode(Tags.TAG_PREPARE_DOWNLOAD_REQUEST, tlvConcat(...parts));
115
+ }
116
+
117
+ export function decodePrepareDownloadResponse(data: Uint8Array): PrepareDownloadDecodedResponse {
118
+ const root = tlvDecode(data);
119
+ if (root.tag !== Tags.TAG_PREPARE_DOWNLOAD_REQUEST) {
120
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
121
+ }
122
+
123
+ const errorNode = tlvFind(root, Tags.TAG_PREPARE_DOWNLOAD_RESPONSE_ERROR);
124
+ if (errorNode) {
125
+ throw new Es10Error(-1, 'PrepareDownload failed on eUICC');
126
+ }
127
+
128
+ const okNode = tlvFind(root, Tags.TAG_PREPARE_DOWNLOAD_RESPONSE_OK);
129
+ if (!okNode) {
130
+ throw new Es10Error(-1, 'Unexpected PrepareDownload response format');
131
+ }
132
+
133
+ return { raw: root.raw ?? data };
134
+ }
135
+
136
+ /**
137
+ * Hash a confirmation code per SGP.22 Section 3.1.3:
138
+ * hashCc = SHA-256(SHA-256(confirmationCodeUtf8) || transactionIdHexBytes)
139
+ */
140
+ export async function hashConfirmationCode(code: string, transactionId: string): Promise<Uint8Array> {
141
+ const codeBytes = new TextEncoder().encode(code);
142
+ const firstHash = new Uint8Array(await crypto.subtle.digest('SHA-256', codeBytes.buffer));
143
+
144
+ // transactionId is a hex string — decode to bytes
145
+ const txIdBytes = hexToBytes(transactionId);
146
+
147
+ const combined = tlvConcat(firstHash, txIdBytes);
148
+ return new Uint8Array(await crypto.subtle.digest('SHA-256', combined.buffer as ArrayBuffer));
149
+ }
150
+
151
+ export function encodeCancelSessionRequest(transactionId: Uint8Array, reason: CancelSessionReason): Uint8Array {
152
+ const txIdTlv = tlvEncode(Tags.TAG_CANCEL_SESSION_TRANSACTION_ID, transactionId);
153
+ const reasonTlv = tlvEncode(Tags.TAG_CANCEL_SESSION_REASON, new Uint8Array([reason]));
154
+ return tlvEncode(Tags.TAG_CANCEL_SESSION_REQUEST, tlvConcat(txIdTlv, reasonTlv));
155
+ }
156
+
157
+ function hexToBytes(hex: string): Uint8Array {
158
+ const bytes = new Uint8Array(hex.length / 2);
159
+ for (let i = 0; i < hex.length; i += 2) {
160
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
161
+ }
162
+ return bytes;
163
+ }
@@ -0,0 +1,168 @@
1
+ import { Es10Error } from '../errors.js';
2
+ import { tlvEncode, tlvDecode, tlvFind, tlvFindAll, tlvConcat } from '../tlv.js';
3
+ import { decodeIccid, encodeIccid } from './iccid.js';
4
+ import {
5
+ type Profile,
6
+ ProfileState,
7
+ ProfileClass,
8
+ type EnableProfileResult,
9
+ type DisableProfileResult,
10
+ type DeleteProfileResult,
11
+ } from '../types.js';
12
+ import * as Tags from './tags.js';
13
+
14
+ export function encodeGetEidRequest(): Uint8Array {
15
+ // GetEuiccDataRequest (BF3E) with tag list requesting EID (5A)
16
+ const tagList = tlvEncode(Tags.TAG_TAG_LIST, new Uint8Array([Tags.TAG_EID]));
17
+ return tlvEncode(Tags.TAG_GET_EID_REQUEST, tagList);
18
+ }
19
+
20
+ export function decodeGetEidResponse(data: Uint8Array): string {
21
+ const root = tlvDecode(data);
22
+ if (root.tag !== Tags.TAG_GET_EID_REQUEST) {
23
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
24
+ }
25
+ const eid = tlvFind(root, Tags.TAG_EID);
26
+ if (!eid) {
27
+ throw new Es10Error(-1, 'EID not found in response');
28
+ }
29
+ // EID is hex-encoded bytes
30
+ return Array.from(eid.value)
31
+ .map((b) => b.toString(16).padStart(2, '0').toUpperCase())
32
+ .join('');
33
+ }
34
+
35
+ export function encodeGetProfilesInfoRequest(): Uint8Array {
36
+ // GetProfilesInfo (BF2D) with tag list for the fields we want
37
+ const tagListValue = tlvConcat(
38
+ new Uint8Array([Tags.TAG_ICCID]),
39
+ new Uint8Array([Tags.TAG_ISDP_AID]),
40
+ encodeTagBytes(Tags.TAG_PROFILE_STATE),
41
+ new Uint8Array([Tags.TAG_PROFILE_NICKNAME]),
42
+ new Uint8Array([Tags.TAG_SERVICE_PROVIDER_NAME]),
43
+ new Uint8Array([Tags.TAG_PROFILE_NAME]),
44
+ new Uint8Array([Tags.TAG_PROFILE_CLASS]),
45
+ );
46
+ const tagList = tlvEncode(Tags.TAG_TAG_LIST, tagListValue);
47
+ return tlvEncode(Tags.TAG_PROFILE_INFO_LIST_REQUEST, tagList);
48
+ }
49
+
50
+ /** Encode a multi-byte tag as raw bytes for inclusion in a tag list */
51
+ function encodeTagBytes(tag: number): Uint8Array {
52
+ if (tag <= 0xff) return new Uint8Array([tag]);
53
+ if (tag <= 0xffff) return new Uint8Array([(tag >> 8) & 0xff, tag & 0xff]);
54
+ return new Uint8Array([(tag >> 16) & 0xff, (tag >> 8) & 0xff, tag & 0xff]);
55
+ }
56
+
57
+ export function decodeGetProfilesInfoResponse(data: Uint8Array): Profile[] {
58
+ const root = tlvDecode(data);
59
+ if (root.tag !== Tags.TAG_PROFILE_INFO_LIST_REQUEST) {
60
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
61
+ }
62
+
63
+ // ProfileInfoListResponse is a CHOICE:
64
+ // - profileInfoListOk: SEQUENCE containing [0] profileInfoList (A0 with E3 entries)
65
+ // - profileInfoListError: INTEGER (tag 02) with error code
66
+
67
+ // Check for error response
68
+ const errorNode = tlvFind(root, 0x02);
69
+ if (errorNode) {
70
+ const errorCode = errorNode.value[0] ?? 127;
71
+ throw new Es10Error(errorCode, `GetProfilesInfo failed: error=${errorCode}`);
72
+ }
73
+
74
+ // Look for A0 wrapper (profileInfoList) first, then search directly
75
+ // Some eUICCs may return profiles directly in BF2D without A0 wrapper
76
+ const profileListNode = tlvFind(root, 0xa0);
77
+ const searchNode = profileListNode ?? root;
78
+
79
+ const profiles: Profile[] = [];
80
+ const profileNodes = tlvFindAll(searchNode, Tags.TAG_PROFILE_INFO);
81
+ for (const node of profileNodes) {
82
+ const profile = decodeProfileInfo(node);
83
+ if (profile) profiles.push(profile);
84
+ }
85
+ return profiles;
86
+ }
87
+
88
+ function decodeProfileInfo(node: TlvNode): Profile | null {
89
+ const iccidNode = tlvFind(node, Tags.TAG_ICCID);
90
+ if (!iccidNode) return null;
91
+
92
+ const iccid = decodeIccid(iccidNode.value);
93
+
94
+ const aidNode = tlvFind(node, Tags.TAG_ISDP_AID);
95
+ const stateNode = tlvFind(node, Tags.TAG_PROFILE_STATE);
96
+ const nicknameNode = tlvFind(node, Tags.TAG_PROFILE_NICKNAME);
97
+ const spNameNode = tlvFind(node, Tags.TAG_SERVICE_PROVIDER_NAME);
98
+ const profileNameNode = tlvFind(node, Tags.TAG_PROFILE_NAME);
99
+ const classNode = tlvFind(node, Tags.TAG_PROFILE_CLASS);
100
+
101
+ return {
102
+ iccid,
103
+ isdpAid: aidNode ? toHex(aidNode.value) : undefined,
104
+ state: stateNode ? (stateNode.value[0] as ProfileState) : ProfileState.Disabled,
105
+ nickname: nicknameNode ? textDecode(nicknameNode.value) : undefined,
106
+ serviceProviderName: spNameNode ? textDecode(spNameNode.value) : undefined,
107
+ profileName: profileNameNode ? textDecode(profileNameNode.value) : undefined,
108
+ profileClass: classNode ? (classNode.value[0] as ProfileClass) : undefined,
109
+ };
110
+ }
111
+
112
+ export function encodeEnableProfileRequest(iccid: string, refreshFlag = false): Uint8Array {
113
+ const iccidTlv = tlvEncode(Tags.TAG_ICCID, encodeIccid(iccid));
114
+ const identifier = tlvEncode(Tags.TAG_PROFILE_IDENTIFIER, iccidTlv);
115
+ const refresh = tlvEncode(Tags.TAG_REFRESH_FLAG, new Uint8Array([refreshFlag ? 0x01 : 0x00]));
116
+ return tlvEncode(Tags.TAG_ENABLE_PROFILE_REQUEST, tlvConcat(identifier, refresh));
117
+ }
118
+
119
+ export function decodeEnableProfileResponse(data: Uint8Array): EnableProfileResult {
120
+ return decodeResultResponse(data, Tags.TAG_ENABLE_PROFILE_REQUEST) as EnableProfileResult;
121
+ }
122
+
123
+ export function encodeDisableProfileRequest(iccid: string, refreshFlag = false): Uint8Array {
124
+ const iccidTlv = tlvEncode(Tags.TAG_ICCID, encodeIccid(iccid));
125
+ const identifier = tlvEncode(Tags.TAG_PROFILE_IDENTIFIER, iccidTlv);
126
+ const refresh = tlvEncode(Tags.TAG_REFRESH_FLAG, new Uint8Array([refreshFlag ? 0x01 : 0x00]));
127
+ return tlvEncode(Tags.TAG_DISABLE_PROFILE_REQUEST, tlvConcat(identifier, refresh));
128
+ }
129
+
130
+ export function decodeDisableProfileResponse(data: Uint8Array): DisableProfileResult {
131
+ return decodeResultResponse(data, Tags.TAG_DISABLE_PROFILE_REQUEST) as DisableProfileResult;
132
+ }
133
+
134
+ export function encodeDeleteProfileRequest(iccid: string): Uint8Array {
135
+ // Note: DeleteProfile does NOT use the A0 wrapper (unlike Enable/Disable)
136
+ // Per SGP.22, ICCID (5A) goes directly inside BF33
137
+ const iccidTlv = tlvEncode(Tags.TAG_ICCID, encodeIccid(iccid));
138
+ return tlvEncode(Tags.TAG_DELETE_PROFILE_REQUEST, iccidTlv);
139
+ }
140
+
141
+ export function decodeDeleteProfileResponse(data: Uint8Array): DeleteProfileResult {
142
+ return decodeResultResponse(data, Tags.TAG_DELETE_PROFILE_REQUEST) as DeleteProfileResult;
143
+ }
144
+
145
+ function decodeResultResponse(data: Uint8Array, expectedTag: number): number {
146
+ const root = tlvDecode(data);
147
+ if (root.tag !== expectedTag) {
148
+ throw new Es10Error(-1, `Unexpected response tag: 0x${root.tag.toString(16)}`);
149
+ }
150
+ const result = tlvFind(root, Tags.TAG_RESULT);
151
+ if (!result) {
152
+ throw new Es10Error(-1, 'Result code not found in response');
153
+ }
154
+ return result.value[0];
155
+ }
156
+
157
+ function toHex(bytes: Uint8Array): string {
158
+ return Array.from(bytes)
159
+ .map((b) => b.toString(16).padStart(2, '0').toUpperCase())
160
+ .join('');
161
+ }
162
+
163
+ function textDecode(bytes: Uint8Array): string {
164
+ return new TextDecoder('utf-8').decode(bytes);
165
+ }
166
+
167
+ // Re-export for use by tlvFind in other modules
168
+ import type { TlvNode } from '../tlv.js';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * BCD nibble-swapped encoding/decoding for SIM ICCIDs.
3
+ *
4
+ * ICCID is stored as BCD with each pair of digits having their nibbles swapped.
5
+ * "89" becomes byte 0x98, "01" becomes byte 0x10, etc.
6
+ */
7
+
8
+ export function encodeIccid(iccid: string): Uint8Array {
9
+ // Pad to even length with 'F' if needed
10
+ const padded = iccid.length % 2 === 1 ? iccid + 'F' : iccid;
11
+ const bytes = new Uint8Array(padded.length / 2);
12
+ for (let i = 0; i < padded.length; i += 2) {
13
+ const hi = parseInt(padded[i], 16);
14
+ const lo = parseInt(padded[i + 1], 16);
15
+ // Swap nibbles: first digit goes in low nibble, second in high
16
+ bytes[i / 2] = (lo << 4) | hi;
17
+ }
18
+ return bytes;
19
+ }
20
+
21
+ export function decodeIccid(bytes: Uint8Array): string {
22
+ let result = '';
23
+ for (let i = 0; i < bytes.length; i++) {
24
+ const lo = bytes[i] & 0x0f;
25
+ const hi = (bytes[i] >> 4) & 0x0f;
26
+ result += lo.toString(16).toUpperCase();
27
+ if (hi !== 0x0f) {
28
+ result += hi.toString(16).toUpperCase();
29
+ }
30
+ }
31
+ return result;
32
+ }