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,377 @@
1
+ // Binary structure handling
2
+ type StructType = 'H' | 'B' | 'L';
3
+
4
+ class Struct {
5
+ private readonly types: StructType[];
6
+
7
+ constructor(fmt: string) {
8
+ if (!fmt.startsWith('>')) {
9
+ throw new Error('Only big-endian formats supported');
10
+ }
11
+
12
+ this.types = [];
13
+ for (const ch of fmt.slice(1)) {
14
+ if ('0123456789'.includes(ch)) {
15
+ continue;
16
+ }
17
+ this.types.push(ch as StructType);
18
+ }
19
+ }
20
+
21
+ byteLength(): number {
22
+ let total = 0;
23
+ for (const t of this.types) {
24
+ switch (t) {
25
+ case 'H':
26
+ total += 2;
27
+ break;
28
+ case 'B':
29
+ total += 1;
30
+ break;
31
+ case 'L':
32
+ total += 4;
33
+ break;
34
+ default:
35
+ throw new TypeError('Unsupported type: ' + t);
36
+ }
37
+ }
38
+ return total;
39
+ }
40
+
41
+ pack(...values: number[]): Buffer {
42
+ if (values.length !== this.types.length) {
43
+ throw new TypeError('Incorrect number of values to pack');
44
+ }
45
+ const buf = Buffer.alloc(this.byteLength());
46
+ let offset = 0;
47
+ for (const [i, t] of this.types.entries()) {
48
+ const v = values[i];
49
+ switch (t) {
50
+ case 'H':
51
+ buf.writeUInt16BE(v, offset);
52
+ offset += 2;
53
+ break;
54
+ case 'B':
55
+ buf.writeUInt8(v, offset);
56
+ offset += 1;
57
+ break;
58
+ case 'L':
59
+ buf.writeUInt32BE(v, offset);
60
+ offset += 4;
61
+ break;
62
+ default:
63
+ throw new TypeError('Unsupported type: ' + t);
64
+ }
65
+ }
66
+ return buf;
67
+ }
68
+ }
69
+
70
+ // Struct constants
71
+ const STRUCT_HBBBL = new Struct('>HBBBL');
72
+ const STRUCT_HL = new Struct('>HL');
73
+ const STRUCT_LB = new Struct('>LB');
74
+ const STRUCT_L = new Struct('>L');
75
+ const STRUCT_B = new Struct('>B');
76
+
77
+ // Stream association types
78
+ type StreamAssociation = 'has-stream' | 'no-stream' | 'either';
79
+ const STREAM_ASSOC_HAS_STREAM: StreamAssociation = 'has-stream';
80
+ const STREAM_ASSOC_NO_STREAM: StreamAssociation = 'no-stream';
81
+ const STREAM_ASSOC_EITHER: StreamAssociation = 'either';
82
+
83
+ // Error classes
84
+ class HyperframeError extends Error {}
85
+ class InvalidDataError extends HyperframeError {}
86
+
87
+ // Flag handling
88
+ class Flag {
89
+ name: string;
90
+ bit: number;
91
+
92
+ constructor(name: string, bit: number) {
93
+ this.name = name;
94
+ this.bit = bit;
95
+ }
96
+ }
97
+
98
+ class Flags {
99
+ definedFlags: Flag[];
100
+ flags: Set<string>;
101
+
102
+ constructor(definedFlags: Flag[]) {
103
+ this.definedFlags = definedFlags;
104
+ this.flags = new Set();
105
+ }
106
+
107
+ add(flag: string): void {
108
+ this.flags.add(flag);
109
+ }
110
+
111
+ has(flag: string): boolean {
112
+ return this.flags.has(flag);
113
+ }
114
+
115
+ toString(): string {
116
+ return Array.from(this.flags).join(', ');
117
+ }
118
+ }
119
+
120
+ // Utility function
121
+ function rawDataRepr(data: Buffer | null | undefined): string {
122
+ if (!data || data.length === 0) {
123
+ return 'None';
124
+ }
125
+ let r = data.toString('hex');
126
+ if (r.length > 20) {
127
+ r = r.slice(0, 20) + '\u2026';
128
+ }
129
+ return '<hex:' + r + '>';
130
+ }
131
+
132
+ // Base frame class
133
+ export class Frame {
134
+ protected definedFlags: Flag[] = [];
135
+ type: number | null = null;
136
+ streamAssociation: StreamAssociation | null = null;
137
+ streamId: number;
138
+ flags: Flags;
139
+ bodyLen: number;
140
+
141
+ constructor(streamId: number, flags: string[] = []) {
142
+ this.streamId = streamId;
143
+ this.flags = new Flags(this.definedFlags);
144
+ this.bodyLen = 0;
145
+
146
+ for (const flag of flags) {
147
+ this.flags.add(flag);
148
+ }
149
+
150
+ if (!this.streamId && this.streamAssociation === STREAM_ASSOC_HAS_STREAM) {
151
+ throw new InvalidDataError(
152
+ `Stream ID must be non-zero for ${this.constructor.name}`,
153
+ );
154
+ }
155
+
156
+ if (this.streamId && this.streamAssociation === STREAM_ASSOC_NO_STREAM) {
157
+ throw new InvalidDataError(
158
+ `Stream ID must be zero for ${this.constructor.name} with streamId=${this.streamId}`,
159
+ );
160
+ }
161
+ }
162
+
163
+ toString(): string {
164
+ return `${this.constructor.name}(streamId=${this.streamId}, flags=${this.flags.toString()}): ${this.bodyRepr()}`;
165
+ }
166
+
167
+ serialize(): Buffer {
168
+ const body = this.serializeBody();
169
+ this.bodyLen = body.length;
170
+
171
+ let flagsVal = 0;
172
+ for (const f of this.definedFlags) {
173
+ if (this.flags.has(f.name)) {
174
+ flagsVal |= f.bit;
175
+ }
176
+ }
177
+
178
+ const header = STRUCT_HBBBL.pack(
179
+ (this.bodyLen >> 8) & 0xffff,
180
+ this.bodyLen & 0xff,
181
+ this.type!,
182
+ flagsVal,
183
+ this.streamId & 0x7fffffff,
184
+ );
185
+
186
+ return Buffer.concat([header, body]);
187
+ }
188
+
189
+ serializeBody(): Buffer {
190
+ throw new Error('Not implemented');
191
+ }
192
+
193
+ protected bodyRepr(): string {
194
+ return rawDataRepr(this.serializeBody());
195
+ }
196
+ }
197
+
198
+ // Specific frame implementations
199
+
200
+ export class SettingsFrame extends Frame {
201
+ static MAX_CONCURRENT_STREAMS = 0x03;
202
+ static INITIAL_WINDOW_SIZE = 0x04;
203
+
204
+ settings: Record<number, number>;
205
+
206
+ constructor(
207
+ streamId: number = 0,
208
+ settings: Record<number, number> | null = null,
209
+ flags: string[] = [],
210
+ ) {
211
+ super(streamId, flags);
212
+ this.definedFlags = [new Flag('ACK', 0x01)];
213
+ this.type = 0x04;
214
+ this.streamAssociation = STREAM_ASSOC_NO_STREAM;
215
+
216
+ if (settings && flags.includes('ACK')) {
217
+ throw new InvalidDataError('Settings must be empty if ACK flag is set.');
218
+ }
219
+
220
+ this.settings = settings || {};
221
+ }
222
+
223
+ bodyRepr(): string {
224
+ return `settings=${JSON.stringify(this.settings)}`;
225
+ }
226
+
227
+ serializeBody(): Buffer {
228
+ if (this.flags.has('ACK')) {
229
+ return Buffer.alloc(0);
230
+ }
231
+
232
+ const buffers: Buffer[] = [];
233
+ for (const setting of Object.keys(this.settings)) {
234
+ const buf = STRUCT_HL.pack(
235
+ Number(setting) & 0xff,
236
+ this.settings[Number(setting)],
237
+ );
238
+ buffers.push(buf);
239
+ }
240
+
241
+ return Buffer.concat(buffers);
242
+ }
243
+ }
244
+
245
+ export class DataFrame extends Frame {
246
+ data: Buffer;
247
+ padLength: number;
248
+
249
+ constructor(
250
+ streamId: number,
251
+ data: Buffer | string = Buffer.from(''),
252
+ flags: string[] = [],
253
+ ) {
254
+ super(streamId, flags);
255
+ this.definedFlags = [
256
+ new Flag('END_STREAM', 0x01),
257
+ new Flag('PADDED', 0x08),
258
+ ];
259
+ this.type = 0x0;
260
+ this.streamAssociation = STREAM_ASSOC_HAS_STREAM;
261
+
262
+ this.padLength = 0;
263
+ this.data = Buffer.isBuffer(data) ? data : Buffer.from(data);
264
+ }
265
+
266
+ serializePaddingData(): Buffer {
267
+ if (this.flags.has('PADDED')) {
268
+ return STRUCT_B.pack(this.padLength);
269
+ }
270
+ return Buffer.alloc(0);
271
+ }
272
+
273
+ serializeBody(): Buffer {
274
+ const paddingData = this.serializePaddingData();
275
+ const padding = Buffer.alloc(this.padLength, 0);
276
+ // Ensure data is a Buffer
277
+ if (!Buffer.isBuffer(this.data)) {
278
+ this.data = Buffer.from(this.data);
279
+ }
280
+ const payload = Buffer.concat([paddingData, this.data, padding]);
281
+ this.bodyLen = payload.length;
282
+ return payload;
283
+ }
284
+ }
285
+
286
+ export class HeadersFrame extends Frame {
287
+ data: Buffer;
288
+ padLength: number;
289
+ dependsOn: number;
290
+ streamWeight: number;
291
+ exclusive: boolean;
292
+
293
+ static ALL_FLAGS: Record<string, number> = {
294
+ END_STREAM: 0x01,
295
+ END_HEADERS: 0x04,
296
+ PADDED: 0x08,
297
+ PRIORITY: 0x20,
298
+ };
299
+
300
+ constructor(
301
+ streamId: number,
302
+ data: Buffer | string = Buffer.from(''),
303
+ flags: string[] = [],
304
+ ) {
305
+ super(streamId, flags);
306
+ // Map given flags to Flag objects using ALL_FLAGS
307
+ this.definedFlags = flags.map(
308
+ (flag) => new Flag(flag, HeadersFrame.ALL_FLAGS[flag]),
309
+ );
310
+ this.type = 0x01;
311
+ this.streamAssociation = STREAM_ASSOC_HAS_STREAM;
312
+
313
+ this.padLength = 0;
314
+ this.dependsOn = 0;
315
+ this.streamWeight = 0;
316
+ this.exclusive = false;
317
+ this.data = Buffer.isBuffer(data) ? data : Buffer.from(data);
318
+ }
319
+
320
+ serializePaddingData(): Buffer {
321
+ if (this.flags.has('PADDED')) {
322
+ return STRUCT_B.pack(this.padLength);
323
+ }
324
+ return Buffer.alloc(0);
325
+ }
326
+
327
+ serializePriorityData(): Buffer {
328
+ return STRUCT_LB.pack(
329
+ this.dependsOn + (this.exclusive ? 0x80000000 : 0),
330
+ this.streamWeight,
331
+ );
332
+ }
333
+
334
+ bodyRepr(): string {
335
+ return `exclusive=${this.exclusive}, dependsOn=${this.dependsOn}, streamWeight=${this.streamWeight}, data=${rawDataRepr(this.data)}`;
336
+ }
337
+
338
+ serializeBody(): Buffer {
339
+ const paddingData = this.serializePaddingData();
340
+ const padding = Buffer.alloc(this.padLength, 0);
341
+ let priorityData: Buffer;
342
+ if (this.flags.has('PRIORITY')) {
343
+ priorityData = this.serializePriorityData();
344
+ } else {
345
+ priorityData = Buffer.alloc(0);
346
+ }
347
+ return Buffer.concat([paddingData, priorityData, this.data, padding]);
348
+ }
349
+ }
350
+
351
+ export class WindowUpdateFrame extends Frame {
352
+ windowIncrement: number;
353
+
354
+ constructor(
355
+ streamId: number,
356
+ windowIncrement: number = 0,
357
+ flags: string[] = [],
358
+ ) {
359
+ super(streamId, flags);
360
+ this.definedFlags = [];
361
+ this.type = 0x08;
362
+ this.streamAssociation = STREAM_ASSOC_EITHER;
363
+
364
+ this.windowIncrement = windowIncrement;
365
+ }
366
+
367
+ bodyRepr(): string {
368
+ return `windowIncrement=${this.windowIncrement}`;
369
+ }
370
+
371
+ serializeBody(): Buffer {
372
+ return STRUCT_L.pack(this.windowIncrement & 0x7fffffff);
373
+ }
374
+ }
375
+
376
+ // Exported constants and types
377
+ export { InvalidDataError, STRUCT_HL };
@@ -0,0 +1,152 @@
1
+ import { Socket } from 'net';
2
+
3
+ import { type XPCDictionary } from '../types.js';
4
+ import { Http2Constants, XpcConstants } from './constants.js';
5
+ import {
6
+ DataFrame,
7
+ HeadersFrame,
8
+ SettingsFrame,
9
+ WindowUpdateFrame,
10
+ } from './handshake-frames.js';
11
+ import { type XPCMessage, encodeMessage } from './xpc-protocol.js';
12
+
13
+ export type ChannelId = number;
14
+ export type MessageId = number;
15
+
16
+ class Handshake {
17
+ private _socket: Socket;
18
+ private readonly _nextMessageId: Record<ChannelId, MessageId>;
19
+
20
+ constructor(socket: Socket) {
21
+ this._socket = socket;
22
+ this._nextMessageId = {
23
+ [Http2Constants.ROOT_CHANNEL]: 0,
24
+ [Http2Constants.REPLY_CHANNEL]: 0,
25
+ };
26
+ }
27
+
28
+ async sendFrame(frame: Buffer): Promise<void> {
29
+ return new Promise<void>((resolve, reject) => {
30
+ if (!this._socket.writable) {
31
+ return reject(new Error('Socket is not writable'));
32
+ }
33
+
34
+ this._socket.write(frame, (error: Error | null | undefined) =>
35
+ error ? reject(error) : resolve(),
36
+ );
37
+ });
38
+ }
39
+
40
+ async sendRequest(data: XPCDictionary): Promise<void> {
41
+ const flags: number = XpcConstants.XPC_FLAGS_ALWAYS_SET;
42
+ const requestMessage: XPCMessage = {
43
+ flags,
44
+ id: BigInt(this._nextMessageId[Http2Constants.ROOT_CHANNEL]),
45
+ body: data,
46
+ };
47
+
48
+ const encodedMessage: Buffer = encodeMessage(requestMessage);
49
+ const dataFrame: DataFrame = new DataFrame(
50
+ Http2Constants.ROOT_CHANNEL,
51
+ encodedMessage,
52
+ [],
53
+ );
54
+ await this.sendFrame(dataFrame.serialize());
55
+ }
56
+
57
+ async perform(): Promise<void> {
58
+ try {
59
+ // Step 1: Send HTTP/2 magic sequence with proper error handling
60
+ await new Promise<void>((resolve, reject) => {
61
+ if (!this._socket.writable) {
62
+ return reject(
63
+ new Error('Socket is not writable for HTTP/2 magic sequence'),
64
+ );
65
+ }
66
+
67
+ this._socket.write(Http2Constants.HTTP2_MAGIC, (err) =>
68
+ err ? reject(err) : resolve(),
69
+ );
70
+ });
71
+
72
+ // Step 2: Send SETTINGS frame on stream 0.
73
+ const settings: Record<number, number> = {
74
+ [SettingsFrame.MAX_CONCURRENT_STREAMS]: 100,
75
+ [SettingsFrame.INITIAL_WINDOW_SIZE]: 1048576,
76
+ };
77
+ const settingsFrame: SettingsFrame = new SettingsFrame(0, settings, []);
78
+ await this.sendFrame(settingsFrame.serialize());
79
+
80
+ // Step 3: Send WINDOW_UPDATE frame on stream 0.
81
+ const windowUpdateFrame: WindowUpdateFrame = new WindowUpdateFrame(
82
+ 0,
83
+ 983041,
84
+ );
85
+ await this.sendFrame(windowUpdateFrame.serialize());
86
+
87
+ // Step 4: Send a HEADERS frame on stream 1.
88
+ const headersFrameRoot: HeadersFrame = new HeadersFrame(
89
+ Http2Constants.ROOT_CHANNEL,
90
+ Buffer.from(''),
91
+ ['END_HEADERS'],
92
+ );
93
+ await this.sendFrame(headersFrameRoot.serialize());
94
+
95
+ // Step 5: Send first DataFrame on stream 1 (empty payload).
96
+ await this.sendRequest({});
97
+
98
+ // Step 6: Send second DataFrame on stream 1 with specific flags.
99
+ const dataMessage: XPCMessage = {
100
+ flags: 0x0201,
101
+ id: 0,
102
+ body: null,
103
+ };
104
+ const encodedDataMessage: Buffer = encodeMessage(dataMessage);
105
+ const dataFrame: DataFrame = new DataFrame(
106
+ Http2Constants.ROOT_CHANNEL,
107
+ encodedDataMessage,
108
+ [],
109
+ );
110
+ await this.sendFrame(dataFrame.serialize());
111
+ this._nextMessageId[Http2Constants.ROOT_CHANNEL]++;
112
+
113
+ // Step 7: Send a HEADERS frame on stream 3.
114
+ const headersFrameReply: HeadersFrame = new HeadersFrame(
115
+ Http2Constants.REPLY_CHANNEL,
116
+ Buffer.from(''),
117
+ ['END_HEADERS'],
118
+ );
119
+ await this.sendFrame(headersFrameReply.serialize());
120
+
121
+ // Step 8: Open REPLY_CHANNEL with INIT_HANDSHAKE flags.
122
+ const replyMessage: XPCMessage = {
123
+ flags:
124
+ XpcConstants.XPC_FLAGS_ALWAYS_SET |
125
+ XpcConstants.XPC_FLAGS_INIT_HANDSHAKE,
126
+ id: 0,
127
+ body: null,
128
+ };
129
+ const encodedReplyMessage: Buffer = encodeMessage(replyMessage);
130
+ const replyDataFrame: DataFrame = new DataFrame(
131
+ Http2Constants.REPLY_CHANNEL,
132
+ encodedReplyMessage,
133
+ [],
134
+ );
135
+ await this.sendFrame(replyDataFrame.serialize());
136
+ this._nextMessageId[Http2Constants.REPLY_CHANNEL]++;
137
+
138
+ // Step 9: Send SETTINGS ACK frame.
139
+ const ackFrame: SettingsFrame = new SettingsFrame(0, null, ['ACK']);
140
+ await this.sendFrame(ackFrame.serialize());
141
+ } catch (error) {
142
+ // Provide detailed error information
143
+ throw new Error(
144
+ error instanceof Error
145
+ ? `Handshake failed at step: ${error.message}`
146
+ : 'Unknown handshake error',
147
+ );
148
+ }
149
+ }
150
+ }
151
+
152
+ export default Handshake;