@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,9 +1,14 @@
1
1
  const { ProtocolV2 } = require('../src/protocols');
2
2
  const { parseConfigure } = require('../src/serialization/protobuf/messages');
3
- const sessionModule = require('../src/protocols/v2/session');
4
-
5
- const { ProtocolV2FrameAssembler, ProtocolV2Session, probeProtocolV2 } = sessionModule;
3
+ const {
4
+ ProtocolV2FrameAssembler,
5
+ ProtocolV2SequenceCursor,
6
+ ProtocolV2Session,
7
+ hexToBytes,
8
+ probeProtocolV2,
9
+ } = require('../src/protocols/v2/session');
6
10
  const protocolV2 = require('../src/protocols/v2');
11
+ const { PROTOCOL_V2_FRAME_MAX_BYTES } = require('../src/constants');
7
12
 
8
13
  const protocolV1Messages = parseConfigure({
9
14
  nested: {
@@ -15,6 +20,18 @@ const protocolV1Messages = parseConfigure({
15
20
  },
16
21
  },
17
22
  },
23
+ Failure: {
24
+ fields: {
25
+ code: {
26
+ type: 'uint32',
27
+ id: 1,
28
+ },
29
+ message: {
30
+ type: 'string',
31
+ id: 2,
32
+ },
33
+ },
34
+ },
18
35
  ButtonRequest: {
19
36
  fields: {
20
37
  code: {
@@ -32,6 +49,7 @@ const protocolV1Messages = parseConfigure({
32
49
  MessageType: {
33
50
  values: {
34
51
  MessageType_Success: 2,
52
+ MessageType_Failure: 3,
35
53
  MessageType_ButtonRequest: 26,
36
54
  MessageType_OnekeyGetFeatures: 10025,
37
55
  MessageType_OnekeyFeatures: 10026,
@@ -42,21 +60,22 @@ const protocolV1Messages = parseConfigure({
42
60
 
43
61
  const protocolV2Messages = parseConfigure({
44
62
  nested: {
45
- GetProtoVersion: {
63
+ ProtocolInfoRequest: {
46
64
  fields: {},
47
65
  },
48
- ProtoVersion: {
66
+ ProtocolInfo: {
49
67
  fields: {
50
- major_version: {
68
+ version: {
51
69
  type: 'uint32',
52
70
  id: 1,
53
71
  },
54
- minor_version: {
72
+ supported_messages: {
55
73
  type: 'uint32',
56
74
  id: 2,
75
+ rule: 'repeated',
57
76
  },
58
- patch_version: {
59
- type: 'uint32',
77
+ protobuf_definition: {
78
+ type: 'string',
60
79
  id: 3,
61
80
  },
62
81
  },
@@ -77,21 +96,72 @@ const protocolV2Messages = parseConfigure({
77
96
  },
78
97
  },
79
98
  },
80
- DevFirmwareUpdate: {
81
- fields: {},
99
+ Failure: {
100
+ fields: {
101
+ code: {
102
+ type: 'uint32',
103
+ id: 1,
104
+ },
105
+ subcode: {
106
+ type: 'uint32',
107
+ id: 2,
108
+ },
109
+ message: {
110
+ type: 'string',
111
+ id: 3,
112
+ },
113
+ },
82
114
  },
83
- DevFirmwareInstallProgress: {
115
+ DeviceFirmwareTarget: {
84
116
  fields: {
85
117
  target_id: {
86
118
  type: 'uint32',
87
119
  id: 1,
88
120
  },
89
- progress: {
90
- type: 'uint32',
121
+ path: {
122
+ type: 'string',
91
123
  id: 2,
92
124
  },
93
125
  },
94
126
  },
127
+ DeviceFirmwareUpdateRequest: {
128
+ fields: {
129
+ targets: {
130
+ type: 'DeviceFirmwareTarget',
131
+ id: 1,
132
+ rule: 'repeated',
133
+ },
134
+ },
135
+ },
136
+ DeviceFirmwareUpdateRecord: {
137
+ fields: {
138
+ target_id: {
139
+ type: 'uint32',
140
+ id: 1,
141
+ },
142
+ status: {
143
+ type: 'uint32',
144
+ id: 10,
145
+ },
146
+ payload_version: {
147
+ type: 'uint32',
148
+ id: 20,
149
+ },
150
+ path: {
151
+ type: 'string',
152
+ id: 30,
153
+ },
154
+ },
155
+ },
156
+ DeviceFirmwareUpdateStatus: {
157
+ fields: {
158
+ records: {
159
+ type: 'DeviceFirmwareUpdateRecord',
160
+ id: 1,
161
+ rule: 'repeated',
162
+ },
163
+ },
164
+ },
95
165
  FileWrite: {
96
166
  fields: {},
97
167
  },
@@ -117,13 +187,14 @@ const protocolV2Messages = parseConfigure({
117
187
  },
118
188
  MessageType: {
119
189
  values: {
120
- MessageType_GetProtoVersion: 60200,
121
- MessageType_ProtoVersion: 60201,
190
+ MessageType_ProtocolInfoRequest: 60200,
191
+ MessageType_ProtocolInfo: 60201,
122
192
  MessageType_Ping: 60206,
123
193
  MessageType_Success: 60207,
194
+ MessageType_Failure: 60208,
124
195
  MessageType_FileWrite: 60805,
125
- MessageType_DevFirmwareUpdate: 61000,
126
- MessageType_DevFirmwareInstallProgress: 61001,
196
+ MessageType_DeviceFirmwareUpdateRequest: 61000,
197
+ MessageType_DeviceFirmwareUpdateStatus: 61002,
127
198
  MessageType_PartialNested: 62000,
128
199
  },
129
200
  },
@@ -144,10 +215,10 @@ const rewriteSeq = (frame, seq) => {
144
215
 
145
216
  describe('Protocol V2 framing and session', () => {
146
217
  test('encodes and decodes Protocol V2 protobuf frames', () => {
147
- const frame = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
148
- major_version: 1,
149
- minor_version: 2,
150
- patch_version: 3,
218
+ const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
219
+ version: 1,
220
+ supported_messages: [60200, 60201],
221
+ protobuf_definition: 'proto',
151
222
  });
152
223
 
153
224
  const parsed = protocolV2.decodeFrame(frame);
@@ -155,15 +226,15 @@ describe('Protocol V2 framing and session', () => {
155
226
 
156
227
  const decoded = ProtocolV2.decodeFrame(schemas, frame);
157
228
  expect(decoded).toEqual({
158
- type: 'ProtoVersion',
159
- messageName: 'ProtoVersion',
229
+ type: 'ProtocolInfo',
230
+ messageName: 'ProtocolInfo',
160
231
  messageTypeId: 60201,
161
232
  pbPayload: parsed.pbPayload,
162
233
  seq: parsed.seq,
163
234
  message: {
164
- major_version: 1,
165
- minor_version: 2,
166
- patch_version: 3,
235
+ version: 1,
236
+ supported_messages: [60200, 60201],
237
+ protobuf_definition: 'proto',
167
238
  },
168
239
  });
169
240
  });
@@ -201,10 +272,9 @@ describe('Protocol V2 framing and session', () => {
201
272
  });
202
273
 
203
274
  test('reassembles split Protocol V2 frames and rejects oversized frames', () => {
204
- const frame = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
205
- major_version: 1,
206
- minor_version: 0,
207
- patch_version: 0,
275
+ const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
276
+ version: 1,
277
+ supported_messages: [],
208
278
  });
209
279
  const assembler = new ProtocolV2FrameAssembler();
210
280
 
@@ -215,16 +285,28 @@ describe('Protocol V2 framing and session', () => {
215
285
  expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large');
216
286
  });
217
287
 
288
+ test('enforces the firmware 4200-byte Protocol V2 frame boundary', () => {
289
+ const boundaryFrame = ProtocolV2.encodeFrame(schemas, 'Ping', {
290
+ message: 'x'.repeat(4187),
291
+ });
292
+
293
+ expect(PROTOCOL_V2_FRAME_MAX_BYTES).toBe(4200);
294
+ expect(boundaryFrame).toHaveLength(PROTOCOL_V2_FRAME_MAX_BYTES);
295
+ expect(() =>
296
+ ProtocolV2.encodeFrame(schemas, 'Ping', {
297
+ message: 'x'.repeat(4188),
298
+ })
299
+ ).toThrow('Protocol V2 frame too large: 4201 > 4200');
300
+ });
301
+
218
302
  test('keeps bytes after the first complete frame for the next read', () => {
219
- const first = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
220
- major_version: 1,
221
- minor_version: 0,
222
- patch_version: 0,
303
+ const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
304
+ version: 1,
305
+ supported_messages: [],
223
306
  });
224
- const second = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
225
- major_version: 2,
226
- minor_version: 0,
227
- patch_version: 0,
307
+ const second = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
308
+ version: 2,
309
+ supported_messages: [],
228
310
  });
229
311
  const assembler = new ProtocolV2FrameAssembler();
230
312
  const combined = new Uint8Array(first.length + second.length);
@@ -237,10 +319,9 @@ describe('Protocol V2 framing and session', () => {
237
319
 
238
320
  test('session writes one encoded frame and decodes the response frame', async () => {
239
321
  const written = [];
240
- const response = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
241
- major_version: 2,
242
- minor_version: 0,
243
- patch_version: 1,
322
+ const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
323
+ version: 2,
324
+ supported_messages: [60206],
244
325
  });
245
326
  const session = new ProtocolV2Session({
246
327
  schemas,
@@ -253,20 +334,169 @@ describe('Protocol V2 framing and session', () => {
253
334
  Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)),
254
335
  });
255
336
 
256
- const result = await session.call('GetProtoVersion', {});
337
+ const result = await session.call('ProtocolInfoRequest', {});
257
338
 
258
339
  expect(written).toHaveLength(1);
259
340
  expect(written[0][4]).toBe(1);
260
341
  expect(written[0][5]).toBe(0);
261
342
  expect(protocolV2.decodeFrame(written[0]).messageTypeId).toBe(60200);
262
343
  expect(result).toEqual({
263
- type: 'ProtoVersion',
344
+ type: 'ProtocolInfo',
345
+ message: {
346
+ version: 2,
347
+ supported_messages: [60206],
348
+ protobuf_definition: null,
349
+ },
350
+ });
351
+ });
352
+
353
+ test('session does not log Protocol V2 TX and RX frames', async () => {
354
+ const written = [];
355
+ const logger = { debug: jest.fn() };
356
+ const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
357
+ version: 2,
358
+ supported_messages: [60206],
359
+ protobuf_definition: 'sensitive-schema',
360
+ });
361
+ const session = new ProtocolV2Session({
362
+ schemas,
363
+ router: 1,
364
+ logger,
365
+ logPrefix: 'ProtocolV2 Test',
366
+ writeFrame: frame => {
367
+ written.push(frame);
368
+ return Promise.resolve();
369
+ },
370
+ readFrame: () =>
371
+ Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)),
372
+ });
373
+
374
+ await session.call('ProtocolInfoRequest', {});
375
+
376
+ expect(logger.debug).not.toHaveBeenCalled();
377
+ });
378
+
379
+ test('session does not log RX frame metadata when protobuf payload decoding fails', async () => {
380
+ const logger = { debug: jest.fn() };
381
+ const response = protocolV2.encodeProtobufFrame(
382
+ 60208,
383
+ hexToBytes('0801121648616e646c6572206e6f742072656769737465726564')
384
+ );
385
+ const session = new ProtocolV2Session({
386
+ schemas,
387
+ router: 1,
388
+ logger,
389
+ logPrefix: 'ProtocolV2 Test',
390
+ writeFrame: () => Promise.resolve(),
391
+ readFrame: () => Promise.resolve(response),
392
+ });
393
+
394
+ await expect(session.call('Ping', { message: 'hello' })).rejects.toThrow();
395
+
396
+ expect(logger.debug).not.toHaveBeenCalled();
397
+ });
398
+
399
+ test('session skips Proto Link ACK frames before decoding the protobuf response', async () => {
400
+ const ack = new Uint8Array(8);
401
+ ack[0] = 0x5a;
402
+ ack[1] = 8;
403
+ ack[2] = 0;
404
+ ack[4] = 1;
405
+ ack[5] = 1;
406
+ ack[6] = 1;
407
+ ack[3] = protocolV2.crc8(ack, 3);
408
+ ack[7] = protocolV2.crc8(ack, 7);
409
+
410
+ const response = ProtocolV2.encodeFrame(schemas, 'Success', {
411
+ message: 'ok',
412
+ });
413
+ const readFrame = jest.fn().mockResolvedValueOnce(ack).mockResolvedValueOnce(response);
414
+ const session = new ProtocolV2Session({
415
+ schemas,
416
+ router: 1,
417
+ writeFrame: () => Promise.resolve(),
418
+ readFrame,
419
+ });
420
+
421
+ await expect(session.call('Ping', { message: 'hello' })).resolves.toEqual({
422
+ type: 'Success',
264
423
  message: {
265
- major_version: 2,
266
- minor_version: 0,
267
- patch_version: 1,
424
+ message: 'ok',
268
425
  },
269
426
  });
427
+ expect(readFrame).toHaveBeenCalledTimes(2);
428
+ });
429
+
430
+ test('session ignores an ACK whose sequence does not match the request', async () => {
431
+ const mismatchedAck = new Uint8Array(8);
432
+ mismatchedAck[0] = 0x5a;
433
+ mismatchedAck[1] = 8;
434
+ mismatchedAck[4] = 1;
435
+ mismatchedAck[5] = 1;
436
+ mismatchedAck[6] = 2;
437
+ mismatchedAck[3] = protocolV2.crc8(mismatchedAck, 3);
438
+ mismatchedAck[7] = protocolV2.crc8(mismatchedAck, 7);
439
+ const readFrame = jest
440
+ .fn()
441
+ .mockResolvedValueOnce(mismatchedAck)
442
+ .mockImplementation(() => new Promise(() => {}));
443
+ const session = new ProtocolV2Session({
444
+ schemas,
445
+ router: 1,
446
+ deliveryTimeoutMs: 10,
447
+ writeFrame: () => Promise.resolve(),
448
+ readFrame,
449
+ });
450
+
451
+ await expect(session.call('Ping', { message: 'hello' })).rejects.toThrow(
452
+ 'Protocol V2 delivery timeout after 10ms for Ping'
453
+ );
454
+ expect(readFrame).toHaveBeenCalledTimes(2);
455
+ });
456
+
457
+ test('session keeps waiting for a response after a matching delivery ACK', async () => {
458
+ const ack = new Uint8Array(8);
459
+ ack[0] = 0x5a;
460
+ ack[1] = 8;
461
+ ack[4] = 1;
462
+ ack[5] = 1;
463
+ ack[6] = 1;
464
+ ack[3] = protocolV2.crc8(ack, 3);
465
+ ack[7] = protocolV2.crc8(ack, 7);
466
+ const readFrame = jest
467
+ .fn()
468
+ .mockResolvedValueOnce(ack)
469
+ .mockImplementation(() => new Promise(() => {}));
470
+ const session = new ProtocolV2Session({
471
+ schemas,
472
+ router: 1,
473
+ deliveryTimeoutMs: 10,
474
+ writeFrame: () => Promise.resolve(),
475
+ readFrame,
476
+ });
477
+
478
+ await expect(session.call('Ping', { message: 'hello' }, { timeoutMs: 25 })).rejects.toThrow(
479
+ 'Protocol V2 response timeout after 25ms for Ping'
480
+ );
481
+ });
482
+
483
+ test('session rejects BLE frames above its configured frame limit before writing', async () => {
484
+ const writeFrame = jest.fn().mockResolvedValue(undefined);
485
+ const response = ProtocolV2.encodeFrame(schemas, 'Success', {
486
+ message: 'ok',
487
+ });
488
+ const session = new ProtocolV2Session({
489
+ schemas,
490
+ router: 1,
491
+ maxFrameBytes: 2048,
492
+ writeFrame,
493
+ readFrame: () => Promise.resolve(response),
494
+ });
495
+
496
+ await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow(
497
+ 'Protocol V2 frame too large for transport: 2061 > 2048'
498
+ );
499
+ expect(writeFrame).not.toHaveBeenCalled();
270
500
  });
271
501
 
272
502
  test('session starts response timeout after the frame is written', async () => {
@@ -293,11 +523,10 @@ describe('Protocol V2 framing and session', () => {
293
523
  });
294
524
  });
295
525
 
296
- test('session accepts response frames with a device-owned seq', async () => {
297
- const response = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
298
- major_version: 2,
299
- minor_version: 0,
300
- patch_version: 1,
526
+ test('session logs a device-owned response seq without rejecting the frame', async () => {
527
+ const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
528
+ version: 2,
529
+ supported_messages: [],
301
530
  });
302
531
  const logger = {
303
532
  debug: jest.fn(),
@@ -310,18 +539,18 @@ describe('Protocol V2 framing and session', () => {
310
539
  logger,
311
540
  });
312
541
 
313
- await expect(session.call('GetProtoVersion', {})).resolves.toEqual({
314
- type: 'ProtoVersion',
542
+ await expect(session.call('ProtocolInfoRequest', {})).resolves.toEqual({
543
+ type: 'ProtocolInfo',
315
544
  message: {
316
- major_version: 2,
317
- minor_version: 0,
318
- patch_version: 1,
545
+ version: 2,
546
+ supported_messages: [],
547
+ protobuf_definition: null,
319
548
  },
320
549
  });
321
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('seq differs'));
550
+ expect(logger.debug).not.toHaveBeenCalled();
322
551
  });
323
552
 
324
- test('session logs decoded transmit and receive payloads', async () => {
553
+ test('session does not log transmit or receive payload details', async () => {
325
554
  const response = ProtocolV2.encodeFrame(schemas, 'Success', {
326
555
  message: 'accepted',
327
556
  });
@@ -344,30 +573,7 @@ describe('Protocol V2 framing and session', () => {
344
573
  },
345
574
  });
346
575
 
347
- expect(logger.debug).toHaveBeenCalledWith('[ProtocolV2 Test] TX payload name=Ping', {
348
- message: 'hello',
349
- });
350
- expect(logger.debug).toHaveBeenCalledWith(
351
- '[ProtocolV2 Test] encode raw frame',
352
- expect.objectContaining({
353
- context: 'tx:Ping',
354
- messageTypeId: 60206,
355
- router: 1,
356
- })
357
- );
358
- expect(logger.debug).toHaveBeenCalledWith(
359
- '[ProtocolV2 Test] decode raw frame',
360
- expect.objectContaining({
361
- context: 'rx:Ping',
362
- messageTypeId: 60207,
363
- })
364
- );
365
- expect(logger.debug).toHaveBeenCalledWith(
366
- '[ProtocolV2 Test] RX payload type=Success messageTypeId=60207',
367
- {
368
- message: 'accepted',
369
- }
370
- );
576
+ expect(logger.debug).not.toHaveBeenCalled();
371
577
  });
372
578
 
373
579
  test('session suppresses debug logs for file transfer calls', async () => {
@@ -399,10 +605,9 @@ describe('Protocol V2 framing and session', () => {
399
605
  const stale = ProtocolV2.encodeFrame(schemas, 'Success', {
400
606
  message: 'stale response',
401
607
  });
402
- const response = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
403
- major_version: 2,
404
- minor_version: 0,
405
- patch_version: 1,
608
+ const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
609
+ version: 2,
610
+ supported_messages: [],
406
611
  });
407
612
  const logger = {
408
613
  debug: jest.fn(),
@@ -417,13 +622,13 @@ describe('Protocol V2 framing and session', () => {
417
622
  });
418
623
 
419
624
  await expect(
420
- session.call('GetProtoVersion', {}, { expectedTypes: ['ProtoVersion'] })
625
+ session.call('ProtocolInfoRequest', {}, { expectedTypes: ['ProtocolInfo'] })
421
626
  ).resolves.toEqual({
422
- type: 'ProtoVersion',
627
+ type: 'ProtocolInfo',
423
628
  message: {
424
- major_version: 2,
425
- minor_version: 0,
426
- patch_version: 1,
629
+ version: 2,
630
+ supported_messages: [],
631
+ protobuf_definition: null,
427
632
  },
428
633
  });
429
634
 
@@ -433,9 +638,8 @@ describe('Protocol V2 framing and session', () => {
433
638
 
434
639
  test('session consumes intermediate response frames before returning the final response', async () => {
435
640
  const written = [];
436
- const progress = ProtocolV2.encodeFrame(schemas, 'DevFirmwareInstallProgress', {
437
- target_id: 0,
438
- progress: 42,
641
+ const progress = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', {
642
+ records: [{ target_id: 4, status: 1 }],
439
643
  });
440
644
  const success = ProtocolV2.encodeFrame(schemas, 'Success', {
441
645
  message: 'ok',
@@ -459,20 +663,19 @@ describe('Protocol V2 framing and session', () => {
459
663
  });
460
664
 
461
665
  const result = await session.call(
462
- 'DevFirmwareUpdate',
463
- {},
666
+ 'DeviceFirmwareUpdateRequest',
667
+ { targets: [{ target_id: 4, path: 'vol1:firmware.bin' }] },
464
668
  {
465
- intermediateTypes: ['DevFirmwareInstallProgress'],
669
+ intermediateTypes: ['DeviceFirmwareUpdateStatus'],
466
670
  onIntermediateResponse,
467
671
  }
468
672
  );
469
673
 
470
674
  expect(readFrame).toHaveBeenCalledTimes(2);
471
675
  expect(onIntermediateResponse).toHaveBeenCalledWith({
472
- type: 'DevFirmwareInstallProgress',
676
+ type: 'DeviceFirmwareUpdateStatus',
473
677
  message: {
474
- target_id: 0,
475
- progress: 42,
678
+ records: [{ target_id: 4, status: 1, payload_version: null, path: null }],
476
679
  },
477
680
  });
478
681
  expect(result).toEqual({
@@ -562,7 +765,7 @@ describe('Protocol V2 framing and session', () => {
562
765
  });
563
766
 
564
767
  await expect(
565
- session.call('GetProtoVersion', {}, { timeoutMs: 10, expectedTypes: ['ProtoVersion'] })
768
+ session.call('ProtocolInfoRequest', {}, { timeoutMs: 10, expectedTypes: ['ProtocolInfo'] })
566
769
  ).rejects.toThrow('Protocol V2 response timeout');
567
770
 
568
771
  // Without cancellation the loop would skip this unexpected Success frame
@@ -661,6 +864,61 @@ describe('Protocol V2 framing and session', () => {
661
864
  expect(written.map(frame => frame[6])).toEqual([1, 2, 1]);
662
865
  });
663
866
 
867
+ test('session reuses an injected sequence cursor across recreated sessions', async () => {
868
+ const written = [];
869
+ const cursor = new ProtocolV2SequenceCursor();
870
+ const makeSession = () =>
871
+ new ProtocolV2Session({
872
+ schemas,
873
+ router: 1,
874
+ sequenceCursor: cursor,
875
+ writeFrame: frame => {
876
+ written.push(frame);
877
+ return Promise.resolve();
878
+ },
879
+ readFrame: () => {
880
+ const [frame] = written.slice(-1);
881
+ const { seq } = protocolV2.decodeFrame(frame);
882
+ return Promise.resolve(
883
+ rewriteSeq(ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }), seq)
884
+ );
885
+ },
886
+ });
887
+
888
+ await makeSession().call('Ping', { message: '1' });
889
+ await makeSession().call('Ping', { message: '2' });
890
+
891
+ expect(written.map(frame => frame[6])).toEqual([1, 2]);
892
+ });
893
+
894
+ test('session passes per-call context to frame IO callbacks', async () => {
895
+ const writeContexts = [];
896
+ const readContexts = [];
897
+ const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
898
+ const session = new ProtocolV2Session({
899
+ schemas,
900
+ router: 1,
901
+ generation: 7,
902
+ writeFrame: (_frame, context) => {
903
+ writeContexts.push(context);
904
+ return Promise.resolve();
905
+ },
906
+ readFrame: context => {
907
+ readContexts.push(context);
908
+ return Promise.resolve(response);
909
+ },
910
+ });
911
+
912
+ await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 });
913
+ await session.call('FileWrite', {}, { timeoutMs: 456 });
914
+
915
+ expect(writeContexts).toEqual([
916
+ { messageName: 'Ping', timeoutMs: 123, highVolume: false, generation: 7 },
917
+ { messageName: 'FileWrite', timeoutMs: 456, highVolume: true, generation: 7 },
918
+ ]);
919
+ expect(readContexts).toEqual(writeContexts);
920
+ });
921
+
664
922
  test('assembler throws and resets on frames with an impossible length field', () => {
665
923
  const assembler = new ProtocolV2FrameAssembler();
666
924
  // expectedLen = 0 < 8-byte minimum: without the guard this poisons the
@@ -687,10 +945,9 @@ describe('Protocol V2 framing and session', () => {
687
945
  });
688
946
 
689
947
  test('assembler drain returns every buffered complete frame', () => {
690
- const first = ProtocolV2.encodeFrame(schemas, 'ProtoVersion', {
691
- major_version: 1,
692
- minor_version: 0,
693
- patch_version: 0,
948
+ const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
949
+ version: 1,
950
+ supported_messages: [],
694
951
  });
695
952
  const second = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' });
696
953
  const third = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'last' });
@@ -714,6 +971,26 @@ describe('Protocol V2 framing and session', () => {
714
971
  expect(decoded.type).toBe('ButtonRequest');
715
972
  });
716
973
 
974
+ test('decodes legacy V1 Failure as a Protocol V2 fallback', () => {
975
+ // Some device-side rejection paths still return legacy Failure(type=3)
976
+ // inside a Protocol V2 frame. It must surface as a device Failure, not as
977
+ // a protobuf catalog TypeError.
978
+ const frame = ProtocolV2.encodeFrame(
979
+ { ...schemas, protocolV2: schemas.protocolV1 },
980
+ 'Failure',
981
+ {
982
+ code: 1,
983
+ message: 'Action cancelled',
984
+ }
985
+ );
986
+ const decoded = ProtocolV2.decodeFrame(schemas, frame);
987
+ expect(decoded.type).toBe('Failure');
988
+ expect(decoded.message).toEqual({
989
+ code: 1,
990
+ message: 'Action cancelled',
991
+ });
992
+ });
993
+
717
994
  test('does not fall back to legacy V1 messages outside the allowlist', () => {
718
995
  // OnekeyFeatures exists only in the V1 schema and is not allowlisted.
719
996
  const frame = protocolV2.encodeProtobufFrame(10026, new Uint8Array(0));
@@ -721,7 +998,6 @@ describe('Protocol V2 framing and session', () => {
721
998
  });
722
999
 
723
1000
  test('hexToBytes converts valid hex and rejects malformed input', () => {
724
- const { hexToBytes } = sessionModule;
725
1001
  expect(hexToBytes('5a0102')).toEqual(new Uint8Array([0x5a, 0x01, 0x02]));
726
1002
  expect(hexToBytes('')).toEqual(new Uint8Array(0));
727
1003
  expect(() => hexToBytes('abc')).toThrow('Invalid hex string: odd length');