@onekeyfe/hd-transport 1.2.0-alpha.2 → 1.2.0-alpha.20

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 (58) hide show
  1. package/README.md +1 -1
  2. package/__tests__/messages.test.js +122 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +351 -0
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
  5. package/__tests__/protocol-v2.test.js +384 -108
  6. package/dist/constants.d.ts +5 -3
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/index.d.ts +920 -369
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +587 -251
  11. package/dist/protocols/index.d.ts +11 -5
  12. package/dist/protocols/index.d.ts.map +1 -1
  13. package/dist/protocols/v2/constants.d.ts +1 -0
  14. package/dist/protocols/v2/constants.d.ts.map +1 -1
  15. package/dist/protocols/v2/decode.d.ts +3 -2
  16. package/dist/protocols/v2/decode.d.ts.map +1 -1
  17. package/dist/protocols/v2/encode.d.ts +2 -3
  18. package/dist/protocols/v2/encode.d.ts.map +1 -1
  19. package/dist/protocols/v2/index.d.ts +0 -1
  20. package/dist/protocols/v2/index.d.ts.map +1 -1
  21. package/dist/protocols/v2/link-manager.d.ts +36 -0
  22. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  23. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  24. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  25. package/dist/protocols/v2/session.d.ts +18 -3
  26. package/dist/protocols/v2/session.d.ts.map +1 -1
  27. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  28. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  29. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  30. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  31. package/dist/types/messages.d.ts +534 -225
  32. package/dist/types/messages.d.ts.map +1 -1
  33. package/dist/types/transport.d.ts +5 -3
  34. package/dist/types/transport.d.ts.map +1 -1
  35. package/messages-protocol-v2.json +825 -709
  36. package/package.json +3 -3
  37. package/scripts/protobuf-build.sh +96 -310
  38. package/scripts/protobuf-patches/index.js +1 -0
  39. package/scripts/protobuf-types.js +12 -0
  40. package/src/constants.ts +21 -15
  41. package/src/index.ts +8 -0
  42. package/src/protocols/index.ts +25 -39
  43. package/src/protocols/v2/constants.ts +1 -0
  44. package/src/protocols/v2/crc8.ts +1 -1
  45. package/src/protocols/v2/decode.ts +38 -34
  46. package/src/protocols/v2/encode.ts +2 -28
  47. package/src/protocols/v2/index.ts +0 -1
  48. package/src/protocols/v2/link-manager.ts +162 -0
  49. package/src/protocols/v2/sequence-cursor.ts +10 -0
  50. package/src/protocols/v2/session.ts +132 -178
  51. package/src/protocols/v2/usb-transport-base.ts +194 -0
  52. package/src/serialization/protobuf/decode.ts +4 -1
  53. package/src/serialization/protobuf/messages.ts +6 -1
  54. package/src/types/messages.ts +636 -265
  55. package/src/types/transport.ts +3 -3
  56. package/dist/protocols/v2/debug.d.ts +0 -13
  57. package/dist/protocols/v2/debug.d.ts.map +0 -1
  58. package/src/protocols/v2/debug.ts +0 -26
@@ -1,6 +1,6 @@
1
1
  import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
2
2
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
3
- import { nextProtoSeq } from './encode';
3
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
4
4
  import { ProtocolV2 } from '..';
5
5
  import * as check from '../../utils/highlevel-checks';
6
6
  import { LogBlockCommand } from '../../utils/logBlockCommand';
@@ -13,6 +13,13 @@ export type ProtocolV2Schemas = {
13
13
  protocolV2: Root;
14
14
  };
15
15
 
16
+ export type ProtocolV2CallContext = {
17
+ messageName: string;
18
+ timeoutMs?: number;
19
+ highVolume: boolean;
20
+ generation: number;
21
+ };
22
+
16
23
  type ProtocolLogger = {
17
24
  debug?: (...args: any[]) => void;
18
25
  error?: (...args: any[]) => void;
@@ -22,11 +29,17 @@ export type ProtocolV2SessionOptions = {
22
29
  schemas: ProtocolV2Schemas;
23
30
  router: number;
24
31
  packetSrc?: number;
25
- writeFrame: (frame: Uint8Array) => Promise<void>;
26
- readFrame: () => Promise<Uint8Array>;
32
+ maxFrameBytes?: number;
33
+ prepareCall?: (context: ProtocolV2CallContext) => Promise<void> | void;
34
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise<void>;
35
+ readFrame: (context: ProtocolV2CallContext) => Promise<Uint8Array>;
27
36
  logger?: ProtocolLogger;
28
37
  logPrefix?: string;
29
38
  createTimeoutError?: (name: string, timeoutMs: number) => Error;
39
+ sequenceCursor?: ProtocolV2SequenceCursor;
40
+ generation?: number;
41
+ writeTimeoutMs?: number;
42
+ deliveryTimeoutMs?: number;
30
43
  };
31
44
 
32
45
  export type ProtocolV2CallOptions = {
@@ -37,6 +50,7 @@ export type ProtocolV2CallOptions = {
37
50
  };
38
51
 
39
52
  export { concatUint8Arrays, ProtocolV2FrameAssembler };
53
+ export { ProtocolV2SequenceCursor };
40
54
 
41
55
  export function hexToBytes(hex: string): Uint8Array {
42
56
  const clean = hex.replace(/\s+/g, '');
@@ -59,15 +73,6 @@ export function bytesToHex(bytes: Uint8Array): string {
59
73
  .join('');
60
74
  }
61
75
 
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
76
  const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
72
77
  ...LogBlockCommand,
73
78
  'FilesystemFileRead',
@@ -79,102 +84,6 @@ function shouldReduceProtocolV2Debug(name: string) {
79
84
  return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
80
85
  }
81
86
 
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
87
  const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
179
88
  'Failure',
180
89
  'ButtonRequest',
@@ -229,24 +138,24 @@ export async function withProtocolTimeout<T>(
229
138
  }
230
139
  }
231
140
 
232
- // Watchdog for the write phase only. Writing a single frame (max 4608 bytes)
233
- // is a bounded transport operation, unlike the read phase where long-running
234
- // device operations (firmware install, user confirmation) can legitimately
235
- // take minutes so no default timeout is applied to reads.
236
- export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 30000;
141
+ // A transport write must never keep the serialized device queue pending forever.
142
+ // Callers must classify a timeout as link-fatal and reset the physical adapter so
143
+ // the losing Promise cannot continue on the next connection generation.
144
+ export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 30_000;
145
+ export const PROTOCOL_V2_DELIVERY_WATCHDOG_TIMEOUT_MS = 5_000;
237
146
 
238
147
  export class ProtocolV2Session {
239
148
  private readonly options: ProtocolV2SessionOptions;
240
149
 
150
+ private readonly sequenceCursor: ProtocolV2SequenceCursor;
151
+
241
152
  // Serializes call() invocations: responses are matched only by type, so two
242
153
  // in-flight calls on the same session would steal each other's responses.
243
154
  private pendingCall: Promise<unknown> = Promise.resolve();
244
155
 
245
- // Per-session sequence counter (1-255, wraps skipping 0).
246
- private protoSeq = 0;
247
-
248
156
  constructor(options: ProtocolV2SessionOptions) {
249
157
  this.options = options;
158
+ this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
250
159
  }
251
160
 
252
161
  call(
@@ -271,46 +180,57 @@ export class ProtocolV2Session {
271
180
  schemas,
272
181
  router,
273
182
  packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
183
+ maxFrameBytes,
184
+ prepareCall,
274
185
  writeFrame,
275
186
  readFrame,
276
187
  logger,
277
188
  logPrefix = 'ProtocolV2',
278
189
  createTimeoutError,
190
+ generation = 0,
191
+ writeTimeoutMs = PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
192
+ deliveryTimeoutMs = PROTOCOL_V2_DELIVERY_WATCHDOG_TIMEOUT_MS,
279
193
  } = this.options;
280
194
 
281
195
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
282
- this.protoSeq = nextProtoSeq(this.protoSeq);
196
+ const callContext: ProtocolV2CallContext = {
197
+ messageName: name,
198
+ timeoutMs: callOptions.timeoutMs,
199
+ highVolume: shouldReduceDebug,
200
+ generation,
201
+ };
202
+ await prepareCall?.(callContext);
203
+ const protoSeq = this.sequenceCursor.next();
283
204
  const frame = ProtocolV2.encodeFrame(schemas, name, data, {
284
205
  packetSrc,
285
206
  router,
286
- seq: this.protoSeq,
287
- logger: shouldReduceDebug ? undefined : logger,
288
- logPrefix,
289
- context: `tx:${name}`,
207
+ seq: protoSeq,
290
208
  });
291
- const expectedSeq = frame[6];
209
+ // Suppress Protocol V2 TX frame logs to avoid excessive transport diagnostics.
210
+ /*
211
+ const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
292
212
 
293
213
  if (!shouldReduceDebug) {
294
- logger?.debug?.(
295
- `[${logPrefix}] TX payload name=${name}`,
296
- sanitizeProtocolV2DebugPayload(data)
297
- );
298
- logger?.debug?.(
299
- `[${logPrefix}] TX frame name=${name} len=${frame.length} router=${frame[4]} attr=${
300
- frame[5]
301
- } seq=${expectedSeq} headerHex=${frameHeaderDebugHex(frame)}`
214
+ logger?.debug?.(`[${logPrefix}] TX`, {
215
+ method: name,
216
+ type: name,
217
+ typeId: txMetadata.messageTypeId,
218
+ seq: txMetadata.seq,
219
+ bytes: frame.length,
220
+ });
221
+ }
222
+ */
223
+
224
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
225
+ throw new Error(
226
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
302
227
  );
303
228
  }
304
229
 
305
- // Lenient watchdog on the write phase only — see
306
- // PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
307
230
  await withProtocolTimeout(
308
- writeFrame(frame),
309
- PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
310
- () =>
311
- new Error(
312
- `Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
313
- )
231
+ writeFrame(frame, callContext),
232
+ writeTimeoutMs,
233
+ () => new Error(`Protocol V2 write timeout after ${writeTimeoutMs}ms for ${name}`)
314
234
  );
315
235
 
316
236
  // Cancellation flag for the read loop: when the response timeout fires,
@@ -318,55 +238,64 @@ export class ProtocolV2Session {
318
238
  // consuming frames meant for the next call. The timeout callback flips the
319
239
  // flag so the loop exits and discards any late frame.
320
240
  const cancellation = { cancelled: false };
241
+ let resolveDelivery: () => void = () => undefined;
242
+ let deliveryConfirmed = false;
243
+ const deliveryPromise = new Promise<void>(resolve => {
244
+ resolveDelivery = () => {
245
+ if (deliveryConfirmed) return;
246
+ deliveryConfirmed = true;
247
+ resolve();
248
+ };
249
+ });
321
250
 
322
251
  const readResponse = async (): Promise<MessageFromOneKey> => {
323
252
  // Some Protocol V2 operations emit progress notifications before the
324
253
  // terminal response. Consume those frames here so callers still see a
325
254
  // request/terminal-response shaped API.
326
255
  while (!cancellation.cancelled) {
327
- const rxFrame = await readFrame();
256
+ const rxFrame = await readFrame(callContext);
328
257
  if (cancellation.cancelled) {
329
258
  // Timed out while waiting: drop the late frame and stop reading.
330
259
  break;
331
260
  }
332
- if (!shouldReduceDebug) {
333
- logger?.debug?.(
334
- `[${logPrefix}] RX frame len=${rxFrame.length} router=${rxFrame[4]} attr=${
335
- rxFrame[5]
336
- } seq=${rxFrame[6]} headerHex=${frameHeaderDebugHex(rxFrame)}`
337
- );
338
- }
339
- const decoded = ProtocolV2.decodeFrame(schemas, rxFrame, {
340
- logger: shouldReduceDebug ? undefined : logger,
341
- logPrefix,
342
- context: `rx:${name}`,
343
- });
344
- if (!shouldReduceDebug && decoded.seq !== expectedSeq) {
345
- logger?.debug?.(
346
- `[${logPrefix}] seq differs for ${name}: tx=${expectedSeq}, rx=${decoded.seq}`
347
- );
348
- }
349
- if (!shouldReduceDebug) {
350
- logger?.debug?.(
351
- `[${logPrefix}] TX name=${name} seq=${expectedSeq} | RX seq=${decoded.seq} messageTypeId=${decoded.messageTypeId} pbPayload=${decoded.pbPayload.length}B`
352
- );
353
- logger?.debug?.(
354
- `[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
355
- sanitizeProtocolV2DebugPayload(decoded.message)
356
- );
357
- }
358
-
359
- const response = check.call(decoded);
360
- if (callOptions.intermediateTypes?.includes(response.type)) {
361
- callOptions.onIntermediateResponse?.(response);
362
- } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
363
- return response;
364
- } else if (!shouldReduceDebug) {
365
- logger?.debug?.(
366
- `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
367
- '|'
368
- )} got=${response.type}`
369
- );
261
+ const ackSequence = ProtocolV2.getAckSequence(rxFrame);
262
+ if (ackSequence !== undefined) {
263
+ if (ackSequence === protoSeq) {
264
+ resolveDelivery();
265
+ }
266
+ } else {
267
+ // Some firmware versions return the protobuf response without a separate
268
+ // ACK. Receiving any valid response frame proves that the request arrived.
269
+ resolveDelivery();
270
+ // Suppress Protocol V2 RX frame logs to avoid excessive transport diagnostics.
271
+ /*
272
+ const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
273
+
274
+ if (!shouldReduceDebug) {
275
+ logger?.debug?.(`[${logPrefix}] RX`, {
276
+ method: name,
277
+ type: rxMetadata.type,
278
+ typeId: rxMetadata.messageTypeId,
279
+ seq: rxMetadata.seq,
280
+ bytes: rxFrame.length,
281
+ });
282
+ }
283
+ */
284
+
285
+ const decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
286
+
287
+ const response = check.call(decoded);
288
+ if (callOptions.intermediateTypes?.includes(response.type)) {
289
+ callOptions.onIntermediateResponse?.(response);
290
+ } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
291
+ return response;
292
+ } else if (!shouldReduceDebug) {
293
+ logger?.debug?.(
294
+ `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
295
+ '|'
296
+ )} got=${response.type}`
297
+ );
298
+ }
370
299
  }
371
300
  }
372
301
  // Only reachable after cancellation; the outer promise has already been
@@ -374,7 +303,7 @@ export class ProtocolV2Session {
374
303
  throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
375
304
  };
376
305
 
377
- return withProtocolTimeout(
306
+ const responsePromise = withProtocolTimeout(
378
307
  readResponse(),
379
308
  callOptions.timeoutMs,
380
309
  () =>
@@ -385,6 +314,31 @@ export class ProtocolV2Session {
385
314
  cancellation.cancelled = true;
386
315
  }
387
316
  );
317
+ const deliverySignal = Promise.race([deliveryPromise, responsePromise.then(() => undefined)]);
318
+ const deliveryWatchdog = withProtocolTimeout(
319
+ deliverySignal,
320
+ deliveryTimeoutMs,
321
+ () => new Error(`Protocol V2 delivery timeout after ${deliveryTimeoutMs}ms for ${name}`),
322
+ () => {
323
+ cancellation.cancelled = true;
324
+ }
325
+ );
326
+
327
+ try {
328
+ const firstResult = await Promise.race([
329
+ deliveryWatchdog.then(() => ({ kind: 'delivered' as const })),
330
+ responsePromise.then(response => ({ kind: 'response' as const, response })),
331
+ ]);
332
+ if (firstResult.kind === 'response') {
333
+ return firstResult.response;
334
+ }
335
+ return await responsePromise;
336
+ } catch (error) {
337
+ cancellation.cancelled = true;
338
+ deliveryWatchdog.catch(() => undefined);
339
+ responsePromise.catch(() => undefined);
340
+ throw error;
341
+ }
388
342
  }
389
343
  }
390
344
 
@@ -0,0 +1,194 @@
1
+ import { ProtocolV2FrameAssembler } from './frame-assembler';
2
+ import { ProtocolV2LinkManager } from './link-manager';
3
+
4
+ import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
+ import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
6
+
7
+ export type ProtocolV2UsbTransportBaseOptions = {
8
+ router: number;
9
+ maxFrameBytes: number;
10
+ logPrefix: string;
11
+ };
12
+
13
+ type ProtocolV2UsbCancellation = {
14
+ generation: number;
15
+ reason?: string;
16
+ promise: Promise<string>;
17
+ cancel: (reason: string) => void;
18
+ };
19
+
20
+ export abstract class ProtocolV2UsbTransportBase<Key> {
21
+ private readonly protocolV2UsbLinks: ProtocolV2LinkManager<Key>;
22
+
23
+ private readonly protocolV2UsbAssemblers = new Map<Key, ProtocolV2FrameAssembler>();
24
+
25
+ private readonly protocolV2UsbGenerations = new Map<Key, number>();
26
+
27
+ private readonly protocolV2UsbCancellations = new Map<Key, ProtocolV2UsbCancellation>();
28
+
29
+ private readonly protocolV2UsbOptions: ProtocolV2UsbTransportBaseOptions;
30
+
31
+ protected constructor(options: ProtocolV2UsbTransportBaseOptions) {
32
+ this.protocolV2UsbOptions = options;
33
+ this.protocolV2UsbLinks = new ProtocolV2LinkManager<Key>({
34
+ getSchemas: () => this.getProtocolV2UsbSchemas(),
35
+ classifyError: () => 'link-fatal',
36
+ onLinkInvalidated: async (key, reason) => {
37
+ this.protocolV2UsbAssemblers.get(key)?.reset();
38
+ await this.resetProtocolV2UsbNativeLink(key, reason);
39
+ await this.onProtocolV2UsbLinkInvalidated(key, reason);
40
+ },
41
+ });
42
+ }
43
+
44
+ protected abstract getProtocolV2UsbSchemas(): ProtocolV2Schemas;
45
+
46
+ protected abstract getProtocolV2UsbLogger(): ProtocolV2SessionOptions['logger'];
47
+
48
+ protected abstract writeProtocolV2UsbPacket(
49
+ key: Key,
50
+ frame: Uint8Array,
51
+ context: ProtocolV2CallContext
52
+ ): Promise<void>;
53
+
54
+ protected abstract readProtocolV2UsbPacket(
55
+ key: Key,
56
+ context: ProtocolV2CallContext
57
+ ): Promise<Uint8Array>;
58
+
59
+ protected abstract resetProtocolV2UsbNativeLink(key: Key, reason: string): Promise<void>;
60
+
61
+ protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
62
+
63
+ protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise<void> | void {
64
+ // Subclasses may clear protocol caches or emit platform-specific diagnostics.
65
+ }
66
+
67
+ protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise<number> {
68
+ await this.protocolV2UsbLinks.invalidateLink(key, reason);
69
+ const generation = (this.protocolV2UsbGenerations.get(key) ?? 0) + 1;
70
+ this.protocolV2UsbGenerations.set(key, generation);
71
+ this.protocolV2UsbCancellations.set(key, this.createProtocolV2UsbCancellation(generation));
72
+ this.protocolV2UsbAssemblers.set(
73
+ key,
74
+ new ProtocolV2FrameAssembler(this.protocolV2UsbOptions.maxFrameBytes)
75
+ );
76
+ return generation;
77
+ }
78
+
79
+ protected callProtocolV2Usb(
80
+ key: Key,
81
+ name: string,
82
+ data: Record<string, unknown>,
83
+ options?: TransportCallOptions
84
+ ): Promise<MessageFromOneKey> {
85
+ return this.protocolV2UsbLinks.call(
86
+ key,
87
+ () => this.createProtocolV2UsbAdapter(key),
88
+ name,
89
+ data,
90
+ options
91
+ );
92
+ }
93
+
94
+ protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise<void> {
95
+ return this.protocolV2UsbLinks.invalidateLink(key, reason);
96
+ }
97
+
98
+ protected invalidateAllProtocolV2UsbLinks(reason: string): Promise<void> {
99
+ return this.protocolV2UsbLinks.invalidateAllLinks(reason);
100
+ }
101
+
102
+ protected async disposeProtocolV2UsbLinks(reason: string): Promise<void> {
103
+ await this.protocolV2UsbLinks.dispose(reason);
104
+ this.protocolV2UsbAssemblers.clear();
105
+ this.protocolV2UsbGenerations.clear();
106
+ this.protocolV2UsbCancellations.clear();
107
+ }
108
+
109
+ private createProtocolV2UsbAdapter(key: Key) {
110
+ const generation = this.protocolV2UsbGenerations.get(key);
111
+ if (generation === undefined) {
112
+ throw new Error('Protocol V2 USB generation has not been initialized');
113
+ }
114
+ const cancellation = this.protocolV2UsbCancellations.get(key);
115
+ if (!cancellation || cancellation.generation !== generation) {
116
+ throw new Error('Protocol V2 USB cancellation state has not been initialized');
117
+ }
118
+
119
+ const assertCurrentGeneration = () => {
120
+ if (cancellation.reason) {
121
+ throw new Error(cancellation.reason);
122
+ }
123
+ if (this.protocolV2UsbGenerations.get(key) !== generation) {
124
+ throw new Error('Protocol V2 USB connection generation changed');
125
+ }
126
+ };
127
+
128
+ return {
129
+ router: this.protocolV2UsbOptions.router,
130
+ maxFrameBytes: this.protocolV2UsbOptions.maxFrameBytes,
131
+ generation,
132
+ prepareCall: () => {
133
+ assertCurrentGeneration();
134
+ this.getProtocolV2UsbAssembler(key).reset();
135
+ },
136
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
137
+ assertCurrentGeneration();
138
+ await this.writeProtocolV2UsbPacket(key, frame, context);
139
+ assertCurrentGeneration();
140
+ },
141
+ readFrame: async (context: ProtocolV2CallContext) => {
142
+ assertCurrentGeneration();
143
+ const assembler = this.getProtocolV2UsbAssembler(key);
144
+ let frame = assembler.push(new Uint8Array(0));
145
+ while (!frame) {
146
+ const packetRead = this.readProtocolV2UsbPacket(key, context).then(packet => ({
147
+ packet,
148
+ }));
149
+ const cancelled = cancellation.promise.then(reason => ({ reason }));
150
+ const result = await Promise.race([packetRead, cancelled]);
151
+ if ('reason' in result) {
152
+ throw new Error(result.reason);
153
+ }
154
+ assertCurrentGeneration();
155
+ frame = assembler.push(result.packet);
156
+ }
157
+ assertCurrentGeneration();
158
+ return frame;
159
+ },
160
+ reset: (reason: string) => {
161
+ cancellation.cancel(reason);
162
+ this.protocolV2UsbAssemblers.get(key)?.reset();
163
+ },
164
+ logger: this.getProtocolV2UsbLogger(),
165
+ logPrefix: this.protocolV2UsbOptions.logPrefix,
166
+ createTimeoutError: (messageName: string, timeoutMs: number) =>
167
+ this.createProtocolV2UsbTimeoutError(messageName, timeoutMs),
168
+ };
169
+ }
170
+
171
+ private getProtocolV2UsbAssembler(key: Key): ProtocolV2FrameAssembler {
172
+ const assembler = this.protocolV2UsbAssemblers.get(key);
173
+ if (!assembler) {
174
+ throw new Error('Protocol V2 USB assembler has not been initialized');
175
+ }
176
+ return assembler;
177
+ }
178
+
179
+ private createProtocolV2UsbCancellation(generation: number): ProtocolV2UsbCancellation {
180
+ let resolveCancellation: (reason: string) => void = () => undefined;
181
+ const cancellation: ProtocolV2UsbCancellation = {
182
+ generation,
183
+ promise: new Promise(resolve => {
184
+ resolveCancellation = resolve;
185
+ }),
186
+ cancel: reason => {
187
+ if (cancellation.reason) return;
188
+ cancellation.reason = reason;
189
+ resolveCancellation(reason);
190
+ },
191
+ };
192
+ return cancellation;
193
+ }
194
+ }
@@ -70,7 +70,10 @@ function messageToJSON(Message: Message<Record<string, unknown>>, fields: Type['
70
70
  }
71
71
  // message type
72
72
  else if (field.resolvedType!.fields) {
73
- res[key] = messageToJSON(value, field.resolvedType!.fields);
73
+ // [compatibility]: absent optional sub-messages are not own properties
74
+ // of the decoded instance, so value is undefined here; decode them to
75
+ // null, matching the optional primitive-field convention above.
76
+ res[key] = value == null ? null : messageToJSON(value, (field.resolvedType as Type).fields);
74
77
  } else {
75
78
  throw new Error(`case not handled: ${key}`);
76
79
  }
@@ -30,7 +30,12 @@ export const createMessageFromName = (messages: protobuf.Root, name: string) =>
30
30
  export const createMessageFromType = (messages: protobuf.Root, typeId: number) => {
31
31
  const MessageType = messages.lookupEnum('MessageType');
32
32
 
33
- const messageName = MessageType.valuesById[typeId].replace('MessageType_', '');
33
+ const rawMessageName = MessageType.valuesById[typeId];
34
+ if (!rawMessageName) {
35
+ throw new Error(`MessageType id "${typeId}" is not defined in protobuf schema`);
36
+ }
37
+
38
+ const messageName = rawMessageName.replace('MessageType_', '');
34
39
 
35
40
  const Message = messages.lookupType(messageName);
36
41