@onekeyfe/hd-transport 1.2.0-alpha.7 → 1.2.0-alpha.70

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 (70) hide show
  1. package/__tests__/messages.test.js +220 -0
  2. package/__tests__/protocol-v2-ble-frame-writer.test.js +119 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +437 -0
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +330 -0
  5. package/__tests__/protocol-v2.test.js +464 -53
  6. package/__tests__/transport-log.test.js +79 -0
  7. package/dist/constants.d.ts +4 -1
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/index.d.ts +803 -198
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +826 -228
  12. package/dist/protocols/index.d.ts +14 -5
  13. package/dist/protocols/index.d.ts.map +1 -1
  14. package/dist/protocols/v2/ble-frame-writer.d.ts +15 -0
  15. package/dist/protocols/v2/ble-frame-writer.d.ts.map +1 -0
  16. package/dist/protocols/v2/constants.d.ts +1 -0
  17. package/dist/protocols/v2/constants.d.ts.map +1 -1
  18. package/dist/protocols/v2/decode.d.ts +13 -2
  19. package/dist/protocols/v2/decode.d.ts.map +1 -1
  20. package/dist/protocols/v2/encode.d.ts +2 -3
  21. package/dist/protocols/v2/encode.d.ts.map +1 -1
  22. package/dist/protocols/v2/errors.d.ts +8 -0
  23. package/dist/protocols/v2/errors.d.ts.map +1 -0
  24. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -1
  25. package/dist/protocols/v2/index.d.ts +2 -1
  26. package/dist/protocols/v2/index.d.ts.map +1 -1
  27. package/dist/protocols/v2/link-manager.d.ts +39 -0
  28. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  29. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  30. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  31. package/dist/protocols/v2/session.d.ts +21 -5
  32. package/dist/protocols/v2/session.d.ts.map +1 -1
  33. package/dist/protocols/v2/usb-transport-base.d.ts +32 -0
  34. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  35. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  36. package/dist/serialization/protobuf/encode.d.ts.map +1 -1
  37. package/dist/types/messages.d.ts +386 -110
  38. package/dist/types/messages.d.ts.map +1 -1
  39. package/dist/types/transport.d.ts +15 -3
  40. package/dist/types/transport.d.ts.map +1 -1
  41. package/dist/utils/transportLog.d.ts +9 -0
  42. package/dist/utils/transportLog.d.ts.map +1 -0
  43. package/messages-protocol-v2.json +1138 -441
  44. package/package.json +3 -3
  45. package/scripts/protobuf-build.sh +86 -208
  46. package/scripts/protobuf-patches/index.js +1 -0
  47. package/scripts/protobuf-types.js +19 -1
  48. package/src/constants.ts +28 -14
  49. package/src/index.ts +12 -1
  50. package/src/protocols/index.ts +39 -40
  51. package/src/protocols/v2/ble-frame-writer.ts +77 -0
  52. package/src/protocols/v2/constants.ts +1 -0
  53. package/src/protocols/v2/crc8.ts +1 -1
  54. package/src/protocols/v2/decode.ts +71 -37
  55. package/src/protocols/v2/encode.ts +2 -28
  56. package/src/protocols/v2/errors.ts +25 -0
  57. package/src/protocols/v2/frame-assembler.ts +6 -4
  58. package/src/protocols/v2/index.ts +2 -1
  59. package/src/protocols/v2/link-manager.ts +209 -0
  60. package/src/protocols/v2/sequence-cursor.ts +10 -0
  61. package/src/protocols/v2/session.ts +162 -200
  62. package/src/protocols/v2/usb-transport-base.ts +213 -0
  63. package/src/serialization/protobuf/decode.ts +4 -1
  64. package/src/serialization/protobuf/encode.ts +5 -1
  65. package/src/types/messages.ts +471 -142
  66. package/src/types/transport.ts +16 -3
  67. package/src/utils/transportLog.ts +102 -0
  68. package/dist/protocols/v2/debug.d.ts +0 -13
  69. package/dist/protocols/v2/debug.d.ts.map +0 -1
  70. package/src/protocols/v2/debug.ts +0 -26
@@ -1,6 +1,10 @@
1
- import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
1
+ import {
2
+ PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
3
+ PROTOCOL_V2_PACKET_SRC_COMMAND,
4
+ } from '../../constants';
2
5
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
3
- import { nextProtoSeq } from './encode';
6
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
7
+ import { ProtocolV2LinkError } from './errors';
4
8
  import { ProtocolV2 } from '..';
5
9
  import * as check from '../../utils/highlevel-checks';
6
10
  import { LogBlockCommand } from '../../utils/logBlockCommand';
@@ -8,11 +12,22 @@ import { LogBlockCommand } from '../../utils/logBlockCommand';
8
12
  import type { Root } from 'protobufjs/light';
9
13
  import type { MessageFromOneKey } from '../../types';
10
14
 
15
+ export * from './errors';
16
+
11
17
  export type ProtocolV2Schemas = {
12
18
  protocolV1: Root;
13
19
  protocolV2: Root;
14
20
  };
15
21
 
22
+ export type ProtocolV2CallContext = {
23
+ messageName: string;
24
+ timeoutMs?: number;
25
+ highVolume: boolean;
26
+ writeWithResponse?: boolean;
27
+ generation: number;
28
+ signal: AbortSignal;
29
+ };
30
+
16
31
  type ProtocolLogger = {
17
32
  debug?: (...args: any[]) => void;
18
33
  error?: (...args: any[]) => void;
@@ -22,11 +37,15 @@ export type ProtocolV2SessionOptions = {
22
37
  schemas: ProtocolV2Schemas;
23
38
  router: number;
24
39
  packetSrc?: number;
25
- writeFrame: (frame: Uint8Array) => Promise<void>;
26
- readFrame: () => Promise<Uint8Array>;
40
+ maxFrameBytes?: number;
41
+ prepareCall?: (context: ProtocolV2CallContext) => Promise<void> | void;
42
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise<void>;
43
+ readFrame: (context: ProtocolV2CallContext) => Promise<Uint8Array>;
27
44
  logger?: ProtocolLogger;
28
45
  logPrefix?: string;
29
46
  createTimeoutError?: (name: string, timeoutMs: number) => Error;
47
+ sequenceCursor?: ProtocolV2SequenceCursor;
48
+ generation?: number;
30
49
  };
31
50
 
32
51
  export type ProtocolV2CallOptions = {
@@ -34,9 +53,11 @@ export type ProtocolV2CallOptions = {
34
53
  expectedTypes?: string[];
35
54
  intermediateTypes?: string[];
36
55
  onIntermediateResponse?: (response: MessageFromOneKey) => void;
56
+ writeWithResponse?: boolean;
37
57
  };
38
58
 
39
59
  export { concatUint8Arrays, ProtocolV2FrameAssembler };
60
+ export { ProtocolV2SequenceCursor };
40
61
 
41
62
  export function hexToBytes(hex: string): Uint8Array {
42
63
  const clean = hex.replace(/\s+/g, '');
@@ -59,15 +80,6 @@ export function bytesToHex(bytes: Uint8Array): string {
59
80
  .join('');
60
81
  }
61
82
 
62
- // Frame header bytes worth logging: SOF, len lo/hi, header CRC, router, attr, seq.
63
- // The rest of the frame is protobuf payload and may contain sensitive fields
64
- // (mnemonic words via WordAck, PINs, seeds via LoadDevice, ...), so raw frame
65
- // hex logs must never include it.
66
- const PROTOCOL_V2_DEBUG_HEADER_BYTES = 7;
67
- const PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT = 20;
68
- const PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT = 40;
69
- const PROTOCOL_V2_DEBUG_STRING_LIMIT = 512;
70
- const PROTOCOL_V2_DEBUG_DEPTH_LIMIT = 4;
71
83
  const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
72
84
  ...LogBlockCommand,
73
85
  'FilesystemFileRead',
@@ -79,102 +91,6 @@ function shouldReduceProtocolV2Debug(name: string) {
79
91
  return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
80
92
  }
81
93
 
82
- function frameHeaderDebugHex(frame: Uint8Array): string {
83
- // Only the frame header is dumped as hex; the payload is logged separately
84
- // in sanitized/structured form (sanitizeProtocolV2DebugPayload).
85
- return bytesToHex(frame.slice(0, PROTOCOL_V2_DEBUG_HEADER_BYTES));
86
- }
87
-
88
- function getBinaryByteLength(value: unknown): number | undefined {
89
- if (value instanceof ArrayBuffer) {
90
- return value.byteLength;
91
- }
92
-
93
- if (ArrayBuffer.isView(value)) {
94
- return value.byteLength;
95
- }
96
-
97
- if (typeof Blob !== 'undefined' && value instanceof Blob) {
98
- return value.size;
99
- }
100
-
101
- return undefined;
102
- }
103
-
104
- function summarizeRedactedData(value: unknown): string {
105
- const byteLength = getBinaryByteLength(value);
106
- if (byteLength !== undefined) {
107
- return `[redacted data: ${byteLength} bytes]`;
108
- }
109
-
110
- if (typeof value === 'string') {
111
- return `[redacted data: string length=${value.length}]`;
112
- }
113
-
114
- if (Array.isArray(value)) {
115
- return `[redacted data: array length=${value.length}]`;
116
- }
117
-
118
- if (value && typeof value === 'object') {
119
- return `[redacted data: object keys=${Object.keys(value).length}]`;
120
- }
121
-
122
- return `[redacted data: ${typeof value}]`;
123
- }
124
-
125
- function sanitizeProtocolV2DebugPayload(value: unknown, key = '', depth = 0): unknown {
126
- if (/^(data|payload)$/i.test(key) && value !== null && value !== undefined) {
127
- return summarizeRedactedData(value);
128
- }
129
-
130
- if (/(passphrase|pin|mnemonic|seed|private)/i.test(key)) {
131
- return '[redacted sensitive value]';
132
- }
133
-
134
- const byteLength = getBinaryByteLength(value);
135
- if (byteLength !== undefined) {
136
- return `[binary: ${byteLength} bytes]`;
137
- }
138
-
139
- if (typeof value === 'string') {
140
- return value.length > PROTOCOL_V2_DEBUG_STRING_LIMIT
141
- ? `${value.slice(0, PROTOCOL_V2_DEBUG_STRING_LIMIT)}... (len=${value.length})`
142
- : value;
143
- }
144
-
145
- if (!value || typeof value !== 'object') {
146
- return value;
147
- }
148
-
149
- if (depth >= PROTOCOL_V2_DEBUG_DEPTH_LIMIT) {
150
- return Array.isArray(value)
151
- ? `[array length=${value.length}]`
152
- : `[object keys=${Object.keys(value).length}]`;
153
- }
154
-
155
- if (Array.isArray(value)) {
156
- const items = value
157
- .slice(0, PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT)
158
- .map(item => sanitizeProtocolV2DebugPayload(item, key, depth + 1));
159
- if (value.length > PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT) {
160
- items.push(`... (${value.length - PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT} more)`);
161
- }
162
- return items;
163
- }
164
-
165
- const entries = Object.entries(value).slice(0, PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT);
166
- const sanitized: Record<string, unknown> = {};
167
- entries.forEach(([entryKey, entryValue]) => {
168
- sanitized[entryKey] = sanitizeProtocolV2DebugPayload(entryValue, entryKey, depth + 1);
169
- });
170
- if (Object.keys(value).length > PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT) {
171
- sanitized.__truncated__ = `${
172
- Object.keys(value).length - PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT
173
- } more keys`;
174
- }
175
- return sanitized;
176
- }
177
-
178
94
  const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
179
95
  'Failure',
180
96
  'ButtonRequest',
@@ -207,11 +123,13 @@ export async function withProtocolTimeout<T>(
207
123
  promise: Promise<T>,
208
124
  timeoutMs: number | undefined,
209
125
  createTimeoutError: () => Error,
210
- onTimeout?: () => void
126
+ onTimeout?: () => void,
127
+ abortSignal?: AbortSignal
211
128
  ): Promise<T> {
212
129
  if (!timeoutMs) return promise;
213
130
 
214
131
  let timer: ReturnType<typeof setTimeout> | undefined;
132
+ let abortHandler: (() => void) | undefined;
215
133
  try {
216
134
  return await Promise.race([
217
135
  promise,
@@ -223,29 +141,43 @@ export async function withProtocolTimeout<T>(
223
141
  reject(createTimeoutError());
224
142
  }, timeoutMs);
225
143
  }),
144
+ ...(abortSignal
145
+ ? [
146
+ new Promise<never>((_, reject) => {
147
+ abortHandler = () => {
148
+ reject(new Error('Protocol V2 operation aborted'));
149
+ };
150
+ if (abortSignal.aborted) {
151
+ abortHandler();
152
+ } else {
153
+ abortSignal.addEventListener('abort', abortHandler, { once: true });
154
+ }
155
+ }),
156
+ ]
157
+ : []),
226
158
  ]);
227
159
  } finally {
228
160
  if (timer) clearTimeout(timer);
161
+ if (abortHandler) {
162
+ abortSignal?.removeEventListener('abort', abortHandler);
163
+ }
229
164
  }
230
165
  }
231
166
 
232
- // Write completion is owned by the concrete transport. A Promise.race watchdog
233
- // here can return while libusb/WebUSB is still flushing a large Protocol V2
234
- // frame, which desynchronizes later request/response pairs.
235
- export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0;
236
-
237
167
  export class ProtocolV2Session {
238
168
  private readonly options: ProtocolV2SessionOptions;
239
169
 
170
+ private readonly sequenceCursor: ProtocolV2SequenceCursor;
171
+
240
172
  // Serializes call() invocations: responses are matched only by type, so two
241
173
  // in-flight calls on the same session would steal each other's responses.
242
174
  private pendingCall: Promise<unknown> = Promise.resolve();
243
175
 
244
- // Per-session sequence counter (1-255, wraps skipping 0).
245
- private protoSeq = 0;
176
+ private lastResponseSequence?: number;
246
177
 
247
178
  constructor(options: ProtocolV2SessionOptions) {
248
179
  this.options = options;
180
+ this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
249
181
  }
250
182
 
251
183
  call(
@@ -261,7 +193,7 @@ export class ProtocolV2Session {
261
193
  return result;
262
194
  }
263
195
 
264
- private async executeCall(
196
+ private executeCall(
265
197
  name: string,
266
198
  data: Record<string, unknown>,
267
199
  callOptions: ProtocolV2CallOptions
@@ -270,118 +202,145 @@ export class ProtocolV2Session {
270
202
  schemas,
271
203
  router,
272
204
  packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
205
+ maxFrameBytes,
206
+ prepareCall,
273
207
  writeFrame,
274
208
  readFrame,
275
209
  logger,
276
210
  logPrefix = 'ProtocolV2',
277
211
  createTimeoutError,
212
+ generation = 0,
278
213
  } = this.options;
214
+ const timeoutMs =
215
+ callOptions.timeoutMs && callOptions.timeoutMs > 0
216
+ ? callOptions.timeoutMs
217
+ : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
218
+ const abortController = new AbortController();
279
219
 
280
220
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
281
- this.protoSeq = nextProtoSeq(this.protoSeq);
282
- const frame = ProtocolV2.encodeFrame(schemas, name, data, {
283
- packetSrc,
284
- router,
285
- seq: this.protoSeq,
286
- logger: shouldReduceDebug ? undefined : logger,
287
- logPrefix,
288
- context: `tx:${name}`,
289
- });
290
- const expectedSeq = frame[6];
291
-
292
- if (!shouldReduceDebug) {
293
- logger?.debug?.(
294
- `[${logPrefix}] TX payload name=${name}`,
295
- sanitizeProtocolV2DebugPayload(data)
296
- );
297
- logger?.debug?.(
298
- `[${logPrefix}] TX frame name=${name} len=${frame.length} router=${frame[4]} attr=${
299
- frame[5]
300
- } seq=${expectedSeq} headerHex=${frameHeaderDebugHex(frame)}`
301
- );
302
- }
221
+ const baseCallContext: ProtocolV2CallContext = {
222
+ messageName: name,
223
+ timeoutMs,
224
+ highVolume: shouldReduceDebug,
225
+ ...(callOptions.writeWithResponse === undefined
226
+ ? {}
227
+ : { writeWithResponse: callOptions.writeWithResponse }),
228
+ generation,
229
+ signal: abortController.signal,
230
+ };
303
231
 
304
- // Lenient watchdog on the write phase only — see
305
- // PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
306
- await withProtocolTimeout(
307
- writeFrame(frame),
308
- PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
309
- () =>
310
- new Error(
311
- `Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
312
- )
313
- );
232
+ const runCall = async (): Promise<MessageFromOneKey> => {
233
+ await prepareCall?.(baseCallContext);
234
+ const protoSeq = this.sequenceCursor.next();
235
+ const frame = ProtocolV2.encodeFrame(schemas, name, data, {
236
+ packetSrc,
237
+ router,
238
+ seq: protoSeq,
239
+ });
240
+
241
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
242
+ throw new Error(
243
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
244
+ );
245
+ }
314
246
 
315
- // Cancellation flag for the read loop: when the response timeout fires,
316
- // Promise.race alone would leave this loop running as a zombie that keeps
317
- // consuming frames meant for the next call. The timeout callback flips the
318
- // flag so the loop exits and discards any late frame.
319
- const cancellation = { cancelled: false };
247
+ await writeFrame(frame, baseCallContext);
320
248
 
321
- const readResponse = async (): Promise<MessageFromOneKey> => {
322
249
  // Some Protocol V2 operations emit progress notifications before the
323
250
  // terminal response. Consume those frames here so callers still see a
324
251
  // request/terminal-response shaped API.
325
- while (!cancellation.cancelled) {
326
- const rxFrame = await readFrame();
327
- if (cancellation.cancelled) {
328
- // Timed out while waiting: drop the late frame and stop reading.
329
- break;
330
- }
331
- if (!shouldReduceDebug) {
332
- logger?.debug?.(
333
- `[${logPrefix}] RX frame len=${rxFrame.length} router=${rxFrame[4]} attr=${
334
- rxFrame[5]
335
- } seq=${rxFrame[6]} headerHex=${frameHeaderDebugHex(rxFrame)}`
252
+ while (!abortController.signal.aborted) {
253
+ const rxFrame = await readFrame(baseCallContext);
254
+ if (abortController.signal.aborted) break;
255
+
256
+ let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
257
+ try {
258
+ header = ProtocolV2.inspectFrameHeader(rxFrame);
259
+ } catch (cause) {
260
+ throw new ProtocolV2LinkError(
261
+ 'frame',
262
+ `Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
263
+ cause
336
264
  );
337
265
  }
338
- const decoded = ProtocolV2.decodeFrame(schemas, rxFrame, {
339
- logger: shouldReduceDebug ? undefined : logger,
340
- logPrefix,
341
- context: `rx:${name}`,
342
- });
343
- if (!shouldReduceDebug && decoded.seq !== expectedSeq) {
344
- logger?.debug?.(
345
- `[${logPrefix}] seq differs for ${name}: tx=${expectedSeq}, rx=${decoded.seq}`
266
+ if (header.router !== router) {
267
+ throw new ProtocolV2LinkError(
268
+ 'router',
269
+ `Protocol V2 router mismatch: expected ${router}, got ${header.router}`
346
270
  );
347
271
  }
348
- if (!shouldReduceDebug) {
349
- logger?.debug?.(
350
- `[${logPrefix}] TX name=${name} seq=${expectedSeq} | RX seq=${decoded.seq} messageTypeId=${decoded.messageTypeId} pbPayload=${decoded.pbPayload.length}B`
351
- );
352
- logger?.debug?.(
353
- `[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
354
- sanitizeProtocolV2DebugPayload(decoded.message)
272
+ if (header.packetSrc !== packetSrc) {
273
+ throw new ProtocolV2LinkError(
274
+ 'packet-source',
275
+ `Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
355
276
  );
356
277
  }
357
278
 
358
- const response = check.call(decoded);
359
- if (callOptions.intermediateTypes?.includes(response.type)) {
360
- callOptions.onIntermediateResponse?.(response);
361
- } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
362
- return response;
363
- } else if (!shouldReduceDebug) {
364
- logger?.debug?.(
365
- `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
366
- '|'
367
- )} got=${response.type}`
279
+ let isAck: boolean;
280
+ try {
281
+ isAck = ProtocolV2.isAckFrame(rxFrame);
282
+ } catch (cause) {
283
+ throw new ProtocolV2LinkError(
284
+ 'frame',
285
+ `Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
286
+ cause
368
287
  );
369
288
  }
289
+ if (isAck) {
290
+ if (header.seq !== protoSeq) {
291
+ throw new ProtocolV2LinkError(
292
+ 'ack-sequence',
293
+ `Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
294
+ );
295
+ }
296
+ } else {
297
+ let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
298
+ try {
299
+ decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
300
+ } catch (cause) {
301
+ throw new ProtocolV2LinkError(
302
+ 'frame',
303
+ `Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
304
+ cause
305
+ );
306
+ }
307
+ // Firmware owns one TX sequence across all channels and packet sources,
308
+ // so frames routed elsewhere create valid gaps in this session.
309
+ if (this.lastResponseSequence === decoded.seq) {
310
+ throw new ProtocolV2LinkError(
311
+ 'response-sequence',
312
+ `Protocol V2 duplicate response sequence: ${decoded.seq}`
313
+ );
314
+ }
315
+ this.lastResponseSequence = decoded.seq;
316
+
317
+ const response = check.call(decoded);
318
+ if (callOptions.intermediateTypes?.includes(response.type)) {
319
+ callOptions.onIntermediateResponse?.(response);
320
+ } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
321
+ return response;
322
+ } else if (!shouldReduceDebug) {
323
+ logger?.debug?.(
324
+ `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
325
+ '|'
326
+ )} got=${response.type}`
327
+ );
328
+ }
329
+ }
370
330
  }
371
- // Only reachable after cancellation; the outer promise has already been
372
- // rejected by the timeout, so this rejection is consumed by the race.
331
+
373
332
  throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
374
333
  };
375
334
 
376
335
  return withProtocolTimeout(
377
- readResponse(),
378
- callOptions.timeoutMs,
336
+ runCall(),
337
+ timeoutMs,
379
338
  () =>
380
339
  createTimeoutError
381
- ? createTimeoutError(name, callOptions.timeoutMs ?? 0)
382
- : new Error(`Protocol V2 response timeout after ${callOptions.timeoutMs}ms for ${name}`),
340
+ ? createTimeoutError(name, timeoutMs)
341
+ : new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
383
342
  () => {
384
- cancellation.cancelled = true;
343
+ abortController.abort();
385
344
  }
386
345
  );
387
346
  }
@@ -412,7 +371,10 @@ export async function probeProtocolV2({
412
371
  const response = await call(
413
372
  'Ping',
414
373
  { message: 'protocol-v2-probe' },
415
- { timeoutMs, expectedTypes: ['Success'] }
374
+ {
375
+ timeoutMs,
376
+ expectedTypes: ['Success'],
377
+ }
416
378
  );
417
379
  if (response.type === 'Success') {
418
380
  return true;