appium-ios-remotexpc 0.0.1

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 (92) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/format-check.yml +43 -0
  3. package/.github/workflows/lint-and-build.yml +40 -0
  4. package/.github/workflows/pr-title.yml +16 -0
  5. package/.github/workflows/publish.js.yml +42 -0
  6. package/.github/workflows/test-validation.yml +40 -0
  7. package/.mocharc.json +8 -0
  8. package/.prettierignore +3 -0
  9. package/.prettierrc +17 -0
  10. package/.releaserc +37 -0
  11. package/CHANGELOG.md +63 -0
  12. package/LICENSE +201 -0
  13. package/README.md +178 -0
  14. package/assets/images/ios-arch.png +0 -0
  15. package/eslint.config.js +45 -0
  16. package/package.json +78 -0
  17. package/scripts/test-tunnel-creation.ts +378 -0
  18. package/src/base-plist-service.ts +83 -0
  19. package/src/base-socket-service.ts +55 -0
  20. package/src/index.ts +34 -0
  21. package/src/lib/apple-tv/constants.ts +83 -0
  22. package/src/lib/apple-tv/errors.ts +31 -0
  23. package/src/lib/apple-tv/tlv/decoder.ts +68 -0
  24. package/src/lib/apple-tv/tlv/encoder.ts +33 -0
  25. package/src/lib/apple-tv/tlv/index.ts +6 -0
  26. package/src/lib/apple-tv/tlv/pairing-tlv.ts +31 -0
  27. package/src/lib/apple-tv/types.ts +58 -0
  28. package/src/lib/apple-tv/utils/buffer-utils.ts +90 -0
  29. package/src/lib/apple-tv/utils/index.ts +2 -0
  30. package/src/lib/apple-tv/utils/uuid-generator.ts +43 -0
  31. package/src/lib/lockdown/index.ts +468 -0
  32. package/src/lib/pair-record/index.ts +8 -0
  33. package/src/lib/pair-record/pair-record.ts +133 -0
  34. package/src/lib/plist/binary-plist-creator.ts +571 -0
  35. package/src/lib/plist/binary-plist-parser.ts +587 -0
  36. package/src/lib/plist/constants.ts +53 -0
  37. package/src/lib/plist/index.ts +54 -0
  38. package/src/lib/plist/length-based-splitter.ts +326 -0
  39. package/src/lib/plist/plist-creator.ts +42 -0
  40. package/src/lib/plist/plist-decoder.ts +135 -0
  41. package/src/lib/plist/plist-encoder.ts +36 -0
  42. package/src/lib/plist/plist-parser.ts +144 -0
  43. package/src/lib/plist/plist-service.ts +231 -0
  44. package/src/lib/plist/unified-plist-creator.ts +19 -0
  45. package/src/lib/plist/unified-plist-parser.ts +25 -0
  46. package/src/lib/plist/utils.ts +376 -0
  47. package/src/lib/remote-xpc/constants.ts +22 -0
  48. package/src/lib/remote-xpc/handshake-frames.ts +377 -0
  49. package/src/lib/remote-xpc/handshake.ts +152 -0
  50. package/src/lib/remote-xpc/remote-xpc-connection.ts +461 -0
  51. package/src/lib/remote-xpc/xpc-protocol.ts +412 -0
  52. package/src/lib/tunnel/index.ts +253 -0
  53. package/src/lib/tunnel/packet-stream-client.ts +185 -0
  54. package/src/lib/tunnel/packet-stream-server.ts +133 -0
  55. package/src/lib/tunnel/tunnel-api-client.ts +234 -0
  56. package/src/lib/tunnel/tunnel-registry-server.ts +410 -0
  57. package/src/lib/types.ts +291 -0
  58. package/src/lib/usbmux/index.ts +630 -0
  59. package/src/lib/usbmux/usbmux-decoder.ts +66 -0
  60. package/src/lib/usbmux/usbmux-encoder.ts +55 -0
  61. package/src/service-connection.ts +79 -0
  62. package/src/services/index.ts +15 -0
  63. package/src/services/ios/base-service.ts +81 -0
  64. package/src/services/ios/diagnostic-service/index.ts +241 -0
  65. package/src/services/ios/diagnostic-service/keys.ts +770 -0
  66. package/src/services/ios/syslog-service/index.ts +387 -0
  67. package/src/services/ios/tunnel-service/index.ts +88 -0
  68. package/src/services.ts +81 -0
  69. package/test/integration/diagnostics-test.ts +44 -0
  70. package/test/integration/read-pair-record-test.ts +39 -0
  71. package/test/integration/tunnel-test.ts +104 -0
  72. package/test/unit/apple-tv/tlv/decoder.spec.ts +144 -0
  73. package/test/unit/apple-tv/tlv/encoder.spec.ts +91 -0
  74. package/test/unit/apple-tv/tlv/pairing-tlv.spec.ts +101 -0
  75. package/test/unit/apple-tv/tlv/tlv-integration.spec.ts +146 -0
  76. package/test/unit/apple-tv/utils/buffer-utils.spec.ts +74 -0
  77. package/test/unit/apple-tv/utils/uuid-generator.spec.ts +39 -0
  78. package/test/unit/fixtures/index.ts +88 -0
  79. package/test/unit/fixtures/usbmuxconnectmessage.bin +0 -0
  80. package/test/unit/fixtures/usbmuxlistdevicemessage.bin +0 -0
  81. package/test/unit/plist/error-handling.spec.ts +101 -0
  82. package/test/unit/plist/fixtures/sample.binary.plist +0 -0
  83. package/test/unit/plist/fixtures/sample.xml.plist +38 -0
  84. package/test/unit/plist/plist-parser.spec.ts +283 -0
  85. package/test/unit/plist/plist.spec.ts +205 -0
  86. package/test/unit/plist/tag-position-handling.spec.ts +90 -0
  87. package/test/unit/plist/unified-plist-parser.spec.ts +227 -0
  88. package/test/unit/plist/utils.spec.ts +249 -0
  89. package/test/unit/plist/xml-cleaning.spec.ts +60 -0
  90. package/test/unit/tunnel/tunnel-registry-server.spec.ts +194 -0
  91. package/test/unit/usbmux/usbmux-specs.ts +71 -0
  92. package/tsconfig.json +36 -0
@@ -0,0 +1,468 @@
1
+ import { logger } from '@appium/support';
2
+ import { Socket } from 'node:net';
3
+ import tls, { type ConnectionOptions, TLSSocket } from 'tls';
4
+
5
+ import { BasePlistService } from '../../base-plist-service.js';
6
+ import { type PairRecord } from '../pair-record/index.js';
7
+ import { PlistService } from '../plist/plist-service.js';
8
+ import type { PlistMessage, PlistValue } from '../types.js';
9
+ import { RelayService, createUsbmux } from '../usbmux/index.js';
10
+
11
+ const log = logger.getLogger('Lockdown');
12
+
13
+ // Constants
14
+ const LABEL = 'appium-internal';
15
+ const DEFAULT_TIMEOUT = 5000;
16
+ const DEFAULT_LOCKDOWN_PORT = 62078;
17
+ const DEFAULT_RELAY_PORT = 2222;
18
+
19
+ // Types and Interfaces
20
+ interface DeviceProperties {
21
+ ConnectionSpeed: number;
22
+ ConnectionType: string;
23
+ DeviceID: number;
24
+ LocationID: number;
25
+ ProductID: number;
26
+ SerialNumber: string;
27
+ USBSerialNumber: string;
28
+ }
29
+
30
+ interface Device {
31
+ DeviceID: number;
32
+ MessageType: string;
33
+ Properties: DeviceProperties;
34
+ }
35
+
36
+ interface LockdownServiceInfo {
37
+ lockdownService: LockdownService;
38
+ device: Device;
39
+ }
40
+
41
+ interface SessionInfo {
42
+ sessionID: string;
43
+ enableSessionSSL: boolean;
44
+ }
45
+
46
+ interface StartSessionRequest {
47
+ Label: string;
48
+ Request: string;
49
+ HostID: string;
50
+ SystemBUID: string;
51
+ }
52
+
53
+ interface StartSessionResponse {
54
+ Request?: string;
55
+ SessionID?: PlistValue;
56
+ EnableSessionSSL?: boolean;
57
+ [key: string]: PlistValue | undefined;
58
+ }
59
+
60
+ interface TLSConfig {
61
+ cert: string;
62
+ key: string;
63
+ }
64
+
65
+ // Error classes for better error handling
66
+ class LockdownError extends Error {
67
+ constructor(message: string) {
68
+ super(message);
69
+ this.name = 'LockdownError';
70
+ }
71
+ }
72
+
73
+ class TLSUpgradeError extends Error {
74
+ constructor(message: string) {
75
+ super(message);
76
+ this.name = 'TLSUpgradeError';
77
+ }
78
+ }
79
+
80
+ class DeviceNotFoundError extends Error {
81
+ constructor(udid: string) {
82
+ super(`Device with UDID ${udid} not found`);
83
+ this.name = 'DeviceNotFoundError';
84
+ }
85
+ }
86
+
87
+ // TLS Manager for handling TLS operations
88
+ class TLSManager {
89
+ private readonly log = logger.getLogger('TLSManager');
90
+
91
+ /**
92
+ * Upgrades a socket to TLS
93
+ */
94
+ async upgradeSocketToTLS(
95
+ socket: Socket,
96
+ tlsOptions: Partial<ConnectionOptions> = {},
97
+ ): Promise<TLSSocket> {
98
+ return new Promise((resolve, reject) => {
99
+ socket.pause();
100
+ this.log.debug('Upgrading socket to TLS...');
101
+
102
+ const secure = tls.connect(
103
+ {
104
+ socket,
105
+ rejectUnauthorized: false,
106
+ minVersion: 'TLSv1.2',
107
+ ...tlsOptions,
108
+ },
109
+ () => {
110
+ this.log.info('TLS handshake completed');
111
+ resolve(secure);
112
+ },
113
+ );
114
+
115
+ secure.on('error', (err) => {
116
+ this.log.error(`TLS socket error: ${err}`);
117
+ reject(new TLSUpgradeError(`TLS socket error: ${err.message}`));
118
+ });
119
+
120
+ socket.on('error', (err) => {
121
+ this.log.error(`Underlying socket error during TLS: ${err}`);
122
+ reject(new TLSUpgradeError(`Socket error during TLS: ${err.message}`));
123
+ });
124
+ });
125
+ }
126
+ }
127
+
128
+ // Device Manager for handling device operations
129
+ class DeviceManager {
130
+ private readonly log = logger.getLogger('DeviceManager');
131
+
132
+ /**
133
+ * Lists all connected devices
134
+ */
135
+ async listDevices(): Promise<Device[]> {
136
+ const usbmux = await createUsbmux();
137
+ try {
138
+ this.log.debug('Listing connected devices...');
139
+ const devices = await usbmux.listDevices();
140
+ this.log.debug(
141
+ `Found ${devices.length} devices: ${devices.map((d) => d.Properties.SerialNumber).join(', ')}`,
142
+ );
143
+ return devices;
144
+ } finally {
145
+ await this.closeUsbmux(usbmux);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Finds a device by UDID
151
+ */
152
+ async findDeviceByUDID(udid: string): Promise<Device> {
153
+ const devices = await this.listDevices();
154
+
155
+ if (!devices || devices.length === 0) {
156
+ throw new LockdownError('No devices connected');
157
+ }
158
+
159
+ const device = devices.find((d) => d.Properties.SerialNumber === udid);
160
+ if (!device) {
161
+ throw new DeviceNotFoundError(udid);
162
+ }
163
+
164
+ this.log.info(
165
+ `Found device: DeviceID=${device.DeviceID}, SerialNumber=${device.Properties.SerialNumber}, ConnectionType=${device.Properties.ConnectionType}`,
166
+ );
167
+
168
+ return device;
169
+ }
170
+
171
+ /**
172
+ * Reads pair record for a device
173
+ */
174
+ async readPairRecord(udid: string): Promise<PairRecord> {
175
+ this.log.debug(`Retrieving pair record for UDID: ${udid}`);
176
+ const usbmux = await createUsbmux();
177
+
178
+ try {
179
+ const record = await usbmux.readPairRecord(udid);
180
+
181
+ if (!record?.HostCertificate || !record.HostPrivateKey) {
182
+ throw new LockdownError('Pair record missing certificate or key');
183
+ }
184
+
185
+ this.log.info('Pair record retrieved successfully');
186
+ return record;
187
+ } catch (err) {
188
+ this.log.error(`Error getting pair record: ${err}`);
189
+ throw err;
190
+ } finally {
191
+ await this.closeUsbmux(usbmux);
192
+ }
193
+ }
194
+
195
+ private async closeUsbmux(usbmux: any): Promise<void> {
196
+ try {
197
+ await usbmux.close();
198
+ } catch (err) {
199
+ this.log.error(`Error closing usbmux: ${err}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ // Main LockdownService class
205
+ export class LockdownService extends BasePlistService {
206
+ private readonly udid: string;
207
+ private tlsService?: PlistService;
208
+ private isTLS = false;
209
+ private tlsUpgradePromise?: Promise<void>;
210
+ private _relayService?: RelayService;
211
+ private readonly tlsManager = new TLSManager();
212
+ private readonly deviceManager = new DeviceManager();
213
+
214
+ constructor(socket: Socket, udid: string, autoSecure = true) {
215
+ super(socket);
216
+ this.udid = udid;
217
+ log.info(`LockdownService initialized for UDID: ${udid}`);
218
+
219
+ if (autoSecure) {
220
+ this.tlsUpgradePromise = this.tryUpgradeToTLS().catch((err) =>
221
+ log.warn(`Auto TLS upgrade failed: ${err.message}`),
222
+ );
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Starts a lockdown session
228
+ */
229
+ async startSession(
230
+ hostID: string,
231
+ systemBUID: string,
232
+ timeout = DEFAULT_TIMEOUT,
233
+ ): Promise<SessionInfo> {
234
+ log.debug(`Starting lockdown session with HostID: ${hostID}`);
235
+
236
+ const request: Record<string, PlistValue> = {
237
+ Label: LABEL,
238
+ Request: 'StartSession',
239
+ HostID: hostID,
240
+ SystemBUID: systemBUID,
241
+ };
242
+
243
+ const response = (await this.sendAndReceive(
244
+ request,
245
+ timeout,
246
+ )) as StartSessionResponse;
247
+
248
+ if (response.Request === 'StartSession' && response.SessionID) {
249
+ const sessionInfo: SessionInfo = {
250
+ sessionID: String(response.SessionID),
251
+ enableSessionSSL: Boolean(response.EnableSessionSSL),
252
+ };
253
+
254
+ log.info(`Lockdown session started, SessionID: ${sessionInfo.sessionID}`);
255
+ return sessionInfo;
256
+ }
257
+
258
+ throw new LockdownError(
259
+ `Unexpected session data: ${JSON.stringify(response)}`,
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Attempts to upgrade the connection to TLS
265
+ */
266
+ async tryUpgradeToTLS(): Promise<void> {
267
+ try {
268
+ const pairRecord = await this.deviceManager.readPairRecord(this.udid);
269
+
270
+ if (!this.validatePairRecord(pairRecord)) {
271
+ log.warn('Invalid pair record for TLS upgrade');
272
+ return;
273
+ }
274
+
275
+ const sessionInfo = await this.startSession(
276
+ pairRecord.HostID!,
277
+ pairRecord.SystemBUID!,
278
+ );
279
+
280
+ if (!sessionInfo.enableSessionSSL) {
281
+ log.info('Device did not request TLS upgrade. Continuing unencrypted.');
282
+ return;
283
+ }
284
+
285
+ await this.performTLSUpgrade(pairRecord);
286
+ } catch (err) {
287
+ log.error(`TLS upgrade failed: ${err}`);
288
+ throw err;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Gets the current socket (TLS or regular)
294
+ */
295
+ public getSocket(): Socket | TLSSocket {
296
+ return this.isTLS && this.tlsService
297
+ ? this.tlsService.getSocket()
298
+ : this.getPlistService().getSocket();
299
+ }
300
+
301
+ /**
302
+ * Sends a message and receives a response
303
+ */
304
+ public async sendAndReceive(
305
+ msg: Record<string, PlistValue>,
306
+ timeout = DEFAULT_TIMEOUT,
307
+ ): Promise<PlistMessage> {
308
+ const service =
309
+ this.isTLS && this.tlsService ? this.tlsService : this._plistService;
310
+ return service.sendPlistAndReceive(msg, timeout);
311
+ }
312
+
313
+ /**
314
+ * Closes the service and associated resources
315
+ */
316
+ public close(): void {
317
+ log.info('Closing LockdownService connections');
318
+
319
+ try {
320
+ this.closeSocket();
321
+ this.stopRelayService();
322
+ } catch (err) {
323
+ log.error(`Error during close: ${err}`);
324
+ throw err;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Sets the relay service for this lockdown instance
330
+ */
331
+ public set relayService(relay: RelayService) {
332
+ this._relayService = relay;
333
+ }
334
+
335
+ /**
336
+ * Gets the relay service for this lockdown instance
337
+ */
338
+ public get relayService(): RelayService | undefined {
339
+ return this._relayService;
340
+ }
341
+
342
+ /**
343
+ * Waits for TLS upgrade to complete if in progress
344
+ */
345
+ public async waitForTLSUpgrade(): Promise<void> {
346
+ if (this.tlsUpgradePromise) {
347
+ await this.tlsUpgradePromise;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Stops the relay service with an optional custom message
353
+ */
354
+ public stopRelayService(
355
+ message = 'Stopping relay server associated with LockdownService',
356
+ ): void {
357
+ const relay = this.relayService;
358
+ if (relay) {
359
+ log.info(message);
360
+ (async () => {
361
+ try {
362
+ await relay.stop();
363
+ log.info('Relay server stopped successfully');
364
+ } catch (err) {
365
+ log.error(`Error stopping relay server: ${err}`);
366
+ }
367
+ })();
368
+ }
369
+ }
370
+
371
+ private validatePairRecord(record: PairRecord): boolean {
372
+ return Boolean(
373
+ record?.HostCertificate &&
374
+ record.HostPrivateKey &&
375
+ record.HostID &&
376
+ record.SystemBUID,
377
+ );
378
+ }
379
+
380
+ private async performTLSUpgrade(pairRecord: PairRecord): Promise<void> {
381
+ const tlsConfig: TLSConfig = {
382
+ cert: pairRecord.HostCertificate!,
383
+ key: pairRecord.HostPrivateKey!,
384
+ };
385
+
386
+ const tlsSocket = await this.tlsManager.upgradeSocketToTLS(
387
+ this.getSocket() as Socket,
388
+ tlsConfig,
389
+ );
390
+
391
+ this.tlsService = new PlistService(tlsSocket);
392
+ this.isTLS = true;
393
+ log.info('Successfully upgraded connection to TLS');
394
+ }
395
+
396
+ private closeSocket(): void {
397
+ if (this.isTLS && this.tlsService) {
398
+ this.tlsService.close();
399
+ } else {
400
+ super.close();
401
+ }
402
+ }
403
+ }
404
+
405
+ // Factory class for creating LockdownService instances
406
+ export class LockdownServiceFactory {
407
+ private readonly deviceManager = new DeviceManager();
408
+
409
+ /**
410
+ * Creates a LockdownService for a specific device UDID
411
+ */
412
+ async createByUDID(
413
+ udid: string,
414
+ port = DEFAULT_LOCKDOWN_PORT,
415
+ autoSecure = true,
416
+ ): Promise<LockdownServiceInfo> {
417
+ log.info(`Creating LockdownService for UDID: ${udid}`);
418
+
419
+ // Find the device
420
+ const device = await this.deviceManager.findDeviceByUDID(udid);
421
+
422
+ // Create relay service
423
+ const relay = new RelayService(device.DeviceID, port, DEFAULT_RELAY_PORT);
424
+ await relay.start();
425
+
426
+ let service: LockdownService | undefined;
427
+ try {
428
+ // Connect through the relay
429
+ const socket = await relay.connect();
430
+ log.debug('Socket connected, creating LockdownService');
431
+
432
+ // Create the lockdown service
433
+ service = new LockdownService(socket, udid, autoSecure);
434
+ service.relayService = relay;
435
+
436
+ // Wait for TLS upgrade if enabled
437
+ if (autoSecure) {
438
+ log.debug('Waiting for TLS upgrade to complete...');
439
+ await service.waitForTLSUpgrade();
440
+ }
441
+
442
+ return { lockdownService: service, device };
443
+ } catch (err) {
444
+ // Clean up relay on error
445
+ service?.stopRelayService('Stopping relay after failure');
446
+ throw err;
447
+ }
448
+ }
449
+ }
450
+
451
+ // Export factory function for backward compatibility
452
+ export async function createLockdownServiceByUDID(
453
+ udid: string,
454
+ port = DEFAULT_LOCKDOWN_PORT,
455
+ autoSecure = true,
456
+ ): Promise<LockdownServiceInfo> {
457
+ const factory = new LockdownServiceFactory();
458
+ return factory.createByUDID(udid, port, autoSecure);
459
+ }
460
+
461
+ // Export the TLS upgrade function for external use
462
+ export function upgradeSocketToTLS(
463
+ socket: Socket,
464
+ tlsOptions: Partial<ConnectionOptions> = {},
465
+ ): Promise<TLSSocket> {
466
+ const tlsManager = new TLSManager();
467
+ return tlsManager.upgradeSocketToTLS(socket, tlsOptions);
468
+ }
@@ -0,0 +1,8 @@
1
+ import {
2
+ type PairRecord,
3
+ getPairRecord,
4
+ processPlistResponse,
5
+ savePairRecord,
6
+ } from './pair-record.js';
7
+
8
+ export { getPairRecord, processPlistResponse, savePairRecord, type PairRecord };
@@ -0,0 +1,133 @@
1
+ import { logger } from '@appium/support';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ const log = logger.getLogger('PairRecord');
6
+
7
+ /**
8
+ * Interface defining the structure of a pair record.
9
+ */
10
+ export interface PairRecord {
11
+ HostID: string | null;
12
+ SystemBUID: string | null;
13
+ HostCertificate: string | null;
14
+ HostPrivateKey: string | null;
15
+ DeviceCertificate: string | null;
16
+ RootCertificate: string | null;
17
+ RootPrivateKey: string | null;
18
+ WiFiMACAddress: string | null;
19
+ EscrowBag: string | null;
20
+ }
21
+
22
+ /**
23
+ * Interface for the raw response from plist.parsePlist
24
+ */
25
+ export interface RawPairRecordResponse {
26
+ HostID: string;
27
+ SystemBUID: string;
28
+ HostCertificate: Buffer;
29
+ HostPrivateKey: Buffer;
30
+ DeviceCertificate: Buffer;
31
+ RootCertificate: Buffer;
32
+ RootPrivateKey: Buffer;
33
+ WiFiMACAddress: string;
34
+ EscrowBag: Buffer;
35
+ }
36
+
37
+ /**
38
+ * Converts a buffer containing PEM data to a string
39
+ * @param buffer - Buffer containing PEM data
40
+ * @returns String representation of the PEM data
41
+ */
42
+ function bufferToPEMString(buffer: Buffer): string {
43
+ return buffer.toString('utf8');
44
+ }
45
+
46
+ /**
47
+ * Processes raw response from plist.parsePlist and formats it into a proper pair-record
48
+ * @param response - Response from plist.parsePlist(data.payload.PairRecordData)
49
+ * @returns Formatted pair-record object with properly structured data
50
+ */
51
+ export function processPlistResponse(
52
+ response: RawPairRecordResponse,
53
+ ): PairRecord {
54
+ return {
55
+ HostID: response.HostID || null,
56
+ SystemBUID: response.SystemBUID || null,
57
+ HostCertificate: response.HostCertificate
58
+ ? bufferToPEMString(response.HostCertificate)
59
+ : null,
60
+ HostPrivateKey: response.HostPrivateKey
61
+ ? bufferToPEMString(response.HostPrivateKey)
62
+ : null,
63
+ DeviceCertificate: response.DeviceCertificate
64
+ ? bufferToPEMString(response.DeviceCertificate)
65
+ : null,
66
+ RootCertificate: response.RootCertificate
67
+ ? bufferToPEMString(response.RootCertificate)
68
+ : null,
69
+ RootPrivateKey: response.RootPrivateKey
70
+ ? bufferToPEMString(response.RootPrivateKey)
71
+ : null,
72
+ WiFiMACAddress: response.WiFiMACAddress || null,
73
+ // For EscrowBag, we need it as a base64 string
74
+ EscrowBag: response.EscrowBag
75
+ ? response.EscrowBag.toString('base64')
76
+ : null,
77
+ };
78
+ }
79
+
80
+ /* --- File storage functions remain unchanged --- */
81
+
82
+ const RECORDS_DIR = path.join(process.cwd(), '../../.records');
83
+
84
+ async function ensureRecordsDirectoryExists(): Promise<void> {
85
+ await fs.promises.mkdir(RECORDS_DIR, { recursive: true, mode: 0o777 });
86
+ }
87
+
88
+ /**
89
+ * Saves a pair record to the filesystem.
90
+ * @param udid - Device UDID.
91
+ * @param pairRecord - Pair record to save.
92
+ * @returns Promise that resolves when record is saved.
93
+ */
94
+ export async function savePairRecord(
95
+ udid: string,
96
+ pairRecord: PairRecord,
97
+ ): Promise<void> {
98
+ await ensureRecordsDirectoryExists();
99
+
100
+ const recordPath = path.join(RECORDS_DIR, `${udid}-record.json`);
101
+ try {
102
+ await fs.promises.writeFile(
103
+ recordPath,
104
+ JSON.stringify(pairRecord, null, 2),
105
+ { mode: 0o777 },
106
+ );
107
+ log.info(`Pair record saved: ${recordPath}`);
108
+ } catch (error) {
109
+ log.error(`Failed to save pair record for ${udid}: ${error}`);
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Gets a saved pair record from the filesystem.
116
+ * @param udid - Device UDID.
117
+ * @returns Promise that resolves with the pair record or null if not found.
118
+ */
119
+ export async function getPairRecord(udid: string): Promise<PairRecord | null> {
120
+ const recordPath = path.join(RECORDS_DIR, `${udid}-record.json`);
121
+
122
+ try {
123
+ const data = await fs.promises.readFile(recordPath, 'utf8');
124
+ return JSON.parse(data) as PairRecord;
125
+ } catch (error) {
126
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
127
+ return null;
128
+ }
129
+
130
+ log.error(`Failed to read pair record for ${udid}: ${error}`);
131
+ throw error;
132
+ }
133
+ }