@onekeyfe/hd-transport 1.1.34-alpha.2 → 1.1.34-alpha.3

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 (93) hide show
  1. package/__tests__/build-receive.test.js +6 -8
  2. package/__tests__/decode-features.test.js +3 -2
  3. package/__tests__/messages.test.js +131 -0
  4. package/__tests__/protocol-v2-link-manager.test.js +351 -0
  5. package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
  6. package/__tests__/protocol-v2.test.js +899 -0
  7. package/dist/constants.d.ts +16 -5
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/index.d.ts +1398 -45
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1102 -85
  12. package/dist/protocols/index.d.ts +50 -0
  13. package/dist/protocols/index.d.ts.map +1 -0
  14. package/dist/protocols/v1/decode.d.ts +11 -0
  15. package/dist/protocols/v1/decode.d.ts.map +1 -0
  16. package/dist/protocols/v1/encode.d.ts +11 -0
  17. package/dist/protocols/v1/encode.d.ts.map +1 -0
  18. package/dist/protocols/v1/index.d.ts +5 -0
  19. package/dist/protocols/v1/index.d.ts.map +1 -0
  20. package/dist/protocols/v1/packets.d.ts +7 -0
  21. package/dist/protocols/v1/packets.d.ts.map +1 -0
  22. package/dist/{serialization → protocols/v1}/receive.d.ts +1 -1
  23. package/dist/protocols/v1/receive.d.ts.map +1 -0
  24. package/dist/protocols/v2/constants.d.ts +8 -0
  25. package/dist/protocols/v2/constants.d.ts.map +1 -0
  26. package/dist/protocols/v2/crc8.d.ts +3 -0
  27. package/dist/protocols/v2/crc8.d.ts.map +1 -0
  28. package/dist/protocols/v2/decode.d.ts +8 -0
  29. package/dist/protocols/v2/decode.d.ts.map +1 -0
  30. package/dist/protocols/v2/encode.d.ts +4 -0
  31. package/dist/protocols/v2/encode.d.ts.map +1 -0
  32. package/dist/protocols/v2/frame-assembler.d.ts +12 -0
  33. package/dist/protocols/v2/frame-assembler.d.ts.map +1 -0
  34. package/dist/protocols/v2/index.d.ts +6 -0
  35. package/dist/protocols/v2/index.d.ts.map +1 -0
  36. package/dist/protocols/v2/link-manager.d.ts +36 -0
  37. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  38. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  39. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  40. package/dist/protocols/v2/session.d.ts +63 -0
  41. package/dist/protocols/v2/session.d.ts.map +1 -0
  42. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  43. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  44. package/dist/serialization/index.d.ts +6 -3
  45. package/dist/serialization/index.d.ts.map +1 -1
  46. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  47. package/dist/serialization/protobuf/messages.d.ts +1 -1
  48. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  49. package/dist/types/messages.d.ts +707 -12
  50. package/dist/types/messages.d.ts.map +1 -1
  51. package/dist/types/transport.d.ts +19 -5
  52. package/dist/types/transport.d.ts.map +1 -1
  53. package/dist/utils/logBlockCommand.d.ts.map +1 -1
  54. package/messages-protocol-v2.json +13544 -0
  55. package/package.json +3 -3
  56. package/scripts/protobuf-patches/TxInputType.js +1 -0
  57. package/scripts/protobuf-patches/index.js +2 -0
  58. package/scripts/protobuf-types.js +233 -18
  59. package/src/constants.ts +48 -6
  60. package/src/index.ts +47 -11
  61. package/src/protocols/index.ts +124 -0
  62. package/src/{serialization/protocol → protocols/v1}/decode.ts +4 -4
  63. package/src/{serialization/protocol → protocols/v1}/encode.ts +18 -13
  64. package/src/protocols/v1/index.ts +4 -0
  65. package/src/protocols/v1/packets.ts +53 -0
  66. package/src/{serialization → protocols/v1}/receive.ts +5 -5
  67. package/src/protocols/v2/constants.ts +7 -0
  68. package/src/protocols/v2/crc8.ts +34 -0
  69. package/src/protocols/v2/decode.ts +89 -0
  70. package/src/protocols/v2/encode.ts +90 -0
  71. package/src/protocols/v2/frame-assembler.ts +98 -0
  72. package/src/protocols/v2/index.ts +5 -0
  73. package/src/protocols/v2/link-manager.ts +162 -0
  74. package/src/protocols/v2/sequence-cursor.ts +10 -0
  75. package/src/protocols/v2/session.ts +339 -0
  76. package/src/protocols/v2/usb-transport-base.ts +194 -0
  77. package/src/serialization/index.ts +6 -5
  78. package/src/serialization/protobuf/decode.ts +7 -0
  79. package/src/serialization/protobuf/messages.ts +14 -5
  80. package/src/types/messages.ts +903 -13
  81. package/src/types/transport.ts +29 -5
  82. package/src/utils/logBlockCommand.ts +9 -1
  83. package/dist/serialization/protocol/decode.d.ts +0 -11
  84. package/dist/serialization/protocol/decode.d.ts.map +0 -1
  85. package/dist/serialization/protocol/encode.d.ts +0 -11
  86. package/dist/serialization/protocol/encode.d.ts.map +0 -1
  87. package/dist/serialization/protocol/index.d.ts +0 -3
  88. package/dist/serialization/protocol/index.d.ts.map +0 -1
  89. package/dist/serialization/receive.d.ts.map +0 -1
  90. package/dist/serialization/send.d.ts +0 -7
  91. package/dist/serialization/send.d.ts.map +0 -1
  92. package/src/serialization/protocol/index.ts +0 -2
  93. package/src/serialization/send.ts +0 -58
@@ -1,30 +1,35 @@
1
1
  import ByteBuffer from 'bytebuffer';
2
2
 
3
- import { BUFFER_SIZE, HEADER_SIZE, MESSAGE_HEADER_BYTE } from '../../constants';
3
+ import {
4
+ PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
5
+ PROTOCOL_V1_ENVELOPE_HEADER_SIZE,
6
+ PROTOCOL_V1_HEADER_BYTE,
7
+ } from '../../constants';
4
8
 
5
9
  type Options<Chunked> = {
6
10
  chunked: Chunked;
7
11
  addTrezorHeaders: boolean;
8
- messageType: number;
12
+ messageTypeId: number;
9
13
  };
10
14
 
11
- function encode(data: ByteBuffer, options: Options<true>): Buffer[];
12
- function encode(data: ByteBuffer, options: Options<false>): Buffer;
13
- function encode(data: any, options: any): any {
14
- const { addTrezorHeaders, chunked, messageType } = options;
15
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
16
- const fullSize = (addTrezorHeaders ? HEADER_SIZE : HEADER_SIZE - 2) + data.limit;
15
+ function encodeEnvelope(data: ByteBuffer, options: Options<true>): Buffer[];
16
+ function encodeEnvelope(data: ByteBuffer, options: Options<false>): Buffer;
17
+ function encodeEnvelope(data: any, options: any): any {
18
+ const { addTrezorHeaders, chunked, messageTypeId } = options;
19
+ const fullSize =
20
+ (addTrezorHeaders ? PROTOCOL_V1_ENVELOPE_HEADER_SIZE : PROTOCOL_V1_ENVELOPE_HEADER_SIZE - 2) +
21
+ Number(data.limit);
17
22
 
18
23
  const encodedByteBuffer = new ByteBuffer(fullSize);
19
24
 
20
25
  if (addTrezorHeaders) {
21
26
  // 2*1 byte
22
- encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
23
- encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
27
+ encodedByteBuffer.writeByte(PROTOCOL_V1_HEADER_BYTE);
28
+ encodedByteBuffer.writeByte(PROTOCOL_V1_HEADER_BYTE);
24
29
  }
25
30
 
26
31
  // 2 bytes
27
- encodedByteBuffer.writeUint16(messageType);
32
+ encodedByteBuffer.writeUint16(messageTypeId);
28
33
 
29
34
  // 4 bytes (so 8 in total)
30
35
  encodedByteBuffer.writeUint32(data.limit);
@@ -39,7 +44,7 @@ function encode(data: any, options: any): any {
39
44
  }
40
45
 
41
46
  const result: Buffer[] = [];
42
- const size = BUFFER_SIZE;
47
+ const size = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE;
43
48
 
44
49
  // How many pieces will there actually be
45
50
  const count = Math.floor((encodedByteBuffer.limit - 1) / size) + 1 || 1;
@@ -56,4 +61,4 @@ function encode(data: any, options: any): any {
56
61
  return result;
57
62
  }
58
63
 
59
- export { encode };
64
+ export { encodeEnvelope };
@@ -0,0 +1,4 @@
1
+ export * from './decode';
2
+ export * from './encode';
3
+ export * from './packets';
4
+ export * from './receive';
@@ -0,0 +1,53 @@
1
+ import ByteBuffer from 'bytebuffer';
2
+
3
+ import { encode as encodeProtobuf } from '../../serialization/protobuf';
4
+ import { encodeEnvelope } from './encode';
5
+ import { createMessageFromName } from '../../serialization/protobuf/messages';
6
+ import { PROTOCOL_V1_CHUNK_PAYLOAD_SIZE, PROTOCOL_V1_REPORT_ID } from '../../constants';
7
+
8
+ import type { Root } from 'protobufjs/light';
9
+
10
+ export function encodeEnvelopeMessage(messages: Root, name: string, data: Record<string, unknown>) {
11
+ const { Message, messageTypeId } = createMessageFromName(messages, name);
12
+
13
+ const buffer = encodeProtobuf(Message, data);
14
+ return encodeEnvelope(buffer, {
15
+ addTrezorHeaders: false,
16
+ chunked: false,
17
+ messageTypeId,
18
+ });
19
+ }
20
+
21
+ export const encodeMessageChunks = (
22
+ messages: Root,
23
+ name: string,
24
+ data: Record<string, unknown>
25
+ ) => {
26
+ const { Message, messageTypeId } = createMessageFromName(messages, name);
27
+ const buffer = encodeProtobuf(Message, data);
28
+ return encodeEnvelope(buffer, {
29
+ addTrezorHeaders: true,
30
+ chunked: true,
31
+ messageTypeId,
32
+ });
33
+ };
34
+
35
+ export const encodeTransportPackets = (
36
+ messages: Root,
37
+ name: string,
38
+ data: Record<string, unknown>
39
+ ) => {
40
+ const encodeBuffers = encodeMessageChunks(messages, name, data);
41
+
42
+ const outBuffers: ByteBuffer[] = [];
43
+
44
+ for (const buf of encodeBuffers) {
45
+ const chunkBuffer = new ByteBuffer(PROTOCOL_V1_CHUNK_PAYLOAD_SIZE + 1);
46
+ chunkBuffer.writeByte(PROTOCOL_V1_REPORT_ID);
47
+ chunkBuffer.append(buf);
48
+ chunkBuffer.reset();
49
+ outBuffers.push(chunkBuffer);
50
+ }
51
+
52
+ return outBuffers;
53
+ };
@@ -1,15 +1,15 @@
1
1
  import ByteBuffer from 'bytebuffer';
2
2
 
3
- import * as decodeProtobuf from './protobuf/decode';
4
- import * as decodeProtocol from './protocol/decode';
5
- import { createMessageFromType } from './protobuf/messages';
3
+ import * as decodeProtobuf from '../../serialization/protobuf/decode';
4
+ import { decodeEnvelope } from './decode';
5
+ import { createMessageFromType } from '../../serialization/protobuf/messages';
6
6
 
7
7
  import type { Root } from 'protobufjs/light';
8
8
 
9
- export function receiveOne(messages: Root, data: string) {
9
+ export function decodeMessage(messages: Root, data: string) {
10
10
  const bytebuffer = ByteBuffer.wrap(data, 'hex');
11
11
 
12
- const { typeId, buffer } = decodeProtocol.decode(bytebuffer);
12
+ const { typeId, buffer } = decodeEnvelope(bytebuffer);
13
13
  const { Message, messageName } = createMessageFromType(messages, typeId);
14
14
  const message = decodeProtobuf.decode(Message, buffer);
15
15
  return {
@@ -0,0 +1,7 @@
1
+ export const PROTO_HEAD_SOF = 0x5a;
2
+ export const PROTO_PRE_HEAD_SIZE = 4;
3
+ export const PROTO_HEAD_CRC_SIZE = 8;
4
+ export const CRC8_INIT = 0x30;
5
+ export const PACKET_SIZE = 4096;
6
+ export const PROTO_DATA_TYPE_PACKET = 0;
7
+ export const PROTO_DATA_TYPE_ACK = 1;
@@ -0,0 +1,34 @@
1
+ import { CRC8_INIT } from './constants';
2
+
3
+ // Protocol V2 帧校验用的 CRC-8 查表,不是设备或业务数据。
4
+ // 表值与固件侧 crc8 实现保持一致;初始值见 CRC8_INIT。
5
+ export const CRC8_TABLE = new Uint8Array([
6
+ 0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41,
7
+ 0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc,
8
+ 0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62,
9
+ 0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff,
10
+ 0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07,
11
+ 0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a,
12
+ 0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24,
13
+ 0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9,
14
+ 0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd,
15
+ 0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50,
16
+ 0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee,
17
+ 0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73,
18
+ 0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b,
19
+ 0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16,
20
+ 0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8,
21
+ 0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35,
22
+ ]);
23
+
24
+ /**
25
+ * Compute CRC-8 over the first len bytes using the firmware-compatible initial value.
26
+ */
27
+ export function crc8(data: Uint8Array, len: number): number {
28
+ let crc = CRC8_INIT;
29
+ for (let i = 0; i < len; i++) {
30
+ // eslint-disable-next-line no-bitwise
31
+ crc = CRC8_TABLE[crc ^ data[i]];
32
+ }
33
+ return crc;
34
+ }
@@ -0,0 +1,89 @@
1
+ import { PROTO_DATA_TYPE_ACK, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
2
+ import { crc8 } from './crc8';
3
+
4
+ export interface ProtoV2Frame {
5
+ /** Little-endian message type ID */
6
+ messageTypeId: number;
7
+ /** Raw protobuf-encoded payload (bytes after the 2-byte messageTypeId) */
8
+ pbPayload: Uint8Array;
9
+ /** Sequence number from the frame header */
10
+ seq: number;
11
+ }
12
+
13
+ function validateFrame(data: Uint8Array): number {
14
+ if (data.length < PROTO_HEAD_CRC_SIZE) {
15
+ throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
16
+ }
17
+
18
+ if (data[0] !== PROTO_HEAD_SOF) {
19
+ throw new Error(
20
+ `Invalid SOF byte: expected 0x5A, got 0x${data[0].toString(16).padStart(2, '0')}`
21
+ );
22
+ }
23
+
24
+ const frameLen = data[1] + data[2] * 256;
25
+
26
+ if (data.length < frameLen) {
27
+ throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`);
28
+ }
29
+
30
+ const expectedHeaderCrc = crc8(data, 3);
31
+ if (data[3] !== expectedHeaderCrc) {
32
+ throw new Error(
33
+ `Header CRC mismatch: expected 0x${expectedHeaderCrc
34
+ .toString(16)
35
+ .padStart(2, '0')}, got 0x${data[3].toString(16).padStart(2, '0')}`
36
+ );
37
+ }
38
+
39
+ const expectedFrameCrc = crc8(data, frameLen - 1);
40
+ if (data[frameLen - 1] !== expectedFrameCrc) {
41
+ throw new Error(
42
+ `Frame CRC mismatch: expected 0x${expectedFrameCrc
43
+ .toString(16)
44
+ .padStart(2, '0')}, got 0x${data[frameLen - 1].toString(16).padStart(2, '0')}`
45
+ );
46
+ }
47
+
48
+ return frameLen;
49
+ }
50
+
51
+ export function isAckFrame(data: Uint8Array): boolean {
52
+ // eslint-disable-next-line no-bitwise
53
+ if (data.length < 6 || (data[5] & 0x03) !== PROTO_DATA_TYPE_ACK) {
54
+ return false;
55
+ }
56
+
57
+ const frameLen = validateFrame(data);
58
+ if (frameLen !== PROTO_HEAD_CRC_SIZE) {
59
+ throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`);
60
+ }
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Parse and validate a Protocol V2 response frame.
66
+ *
67
+ * Validates:
68
+ * - SOF byte (0x5A)
69
+ * - Header CRC (bytes 0-2)
70
+ * - Frame CRC (full frame except last byte)
71
+ *
72
+ * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
73
+ */
74
+ export function decodeFrame(data: Uint8Array): ProtoV2Frame {
75
+ const frameLen = validateFrame(data);
76
+
77
+ const seq = data[6];
78
+ // Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
79
+ const payloadData = data.slice(7, frameLen - 1);
80
+
81
+ if (payloadData.length < 2) {
82
+ throw new Error(`Protocol V2 payload too short (need >=2 bytes for messageTypeId)`);
83
+ }
84
+
85
+ const messageTypeId = payloadData[0] + payloadData[1] * 256;
86
+ const pbPayload = payloadData.slice(2);
87
+
88
+ return { messageTypeId, pbPayload, seq };
89
+ }
@@ -0,0 +1,90 @@
1
+ import { PROTO_DATA_TYPE_PACKET, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
2
+ import { crc8 } from './crc8';
3
+ import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
4
+
5
+ // Default sequence number when callers do not manage one themselves.
6
+ // Stateful per-session sequencing lives in ProtocolV2Session (session.ts),
7
+ // which advances the counter via nextProtoSeq() and passes it in explicitly.
8
+ const PROTO_SEQ_DEFAULT = 1;
9
+
10
+ /**
11
+ * Advance a Protocol V2 sequence counter: 1-255, wraps around skipping 0.
12
+ */
13
+ export function nextProtoSeq(current: number): number {
14
+ return current >= 255 ? 1 : current + 1;
15
+ }
16
+
17
+ /**
18
+ * Build a raw Protocol V2 frame (0x5A framing).
19
+ *
20
+ * Frame layout (PROTO_HEAD_CRC_SIZE = 8 overhead bytes):
21
+ * [0] SOF = 0x5A
22
+ * [1] frameLen low byte
23
+ * [2] frameLen high byte
24
+ * [3] CRC8 of bytes 0-2 (pre-header CRC)
25
+ * [4] router
26
+ * [5] attr = ((packetSrc & 0x0F) << 2) | dataType
27
+ * [6] seq (1-255, wraps skipping 0)
28
+ * [7..N-2] payload
29
+ * [N-1] CRC8 of bytes 0 to N-2 (frame CRC)
30
+ */
31
+ export function encodeFrame(
32
+ payload: Uint8Array | null,
33
+ packetSrc?: number,
34
+ router?: number,
35
+ seq?: number
36
+ ): Uint8Array {
37
+ const resolvedPacketSrc = packetSrc ?? 0;
38
+ const resolvedRouter = router ?? 0;
39
+ const resolvedSeq = seq ?? PROTO_SEQ_DEFAULT;
40
+ if (!Number.isInteger(resolvedSeq) || resolvedSeq < 1 || resolvedSeq > 255) {
41
+ throw new Error(`Protocol V2 seq out of range (1-255): ${resolvedSeq}`);
42
+ }
43
+ const payloadLen = payload ? payload.length : 0;
44
+ const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE;
45
+ if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) {
46
+ throw new Error(`Protocol V2 frame too large: ${frameLen}`);
47
+ }
48
+ const frame = new Uint8Array(frameLen);
49
+
50
+ frame[0] = PROTO_HEAD_SOF;
51
+ frame[1] = frameLen % 256;
52
+ frame[2] = Math.floor(frameLen / 256) % 256;
53
+ frame[3] = 0; // placeholder — filled in below
54
+ frame[4] = resolvedRouter % 256;
55
+ frame[5] = (resolvedPacketSrc % 16) * 4 + (PROTO_DATA_TYPE_PACKET % 4);
56
+ frame[6] = resolvedSeq;
57
+
58
+ // CRC8 over first 3 bytes (SOF + length)
59
+ frame[3] = crc8(frame, 3);
60
+
61
+ if (payload && payloadLen > 0) {
62
+ frame.set(payload, 7);
63
+ }
64
+
65
+ // CRC8 over entire frame except last byte
66
+ frame[frameLen - 1] = crc8(frame, frameLen - 1);
67
+
68
+ return frame;
69
+ }
70
+
71
+ /**
72
+ * Build a Protocol V2 frame carrying a protobuf message.
73
+ *
74
+ * Payload layout:
75
+ * [0-1] messageTypeId as little-endian uint16
76
+ * [2..] protobuf-encoded message bytes
77
+ */
78
+ export function encodeProtobufFrame(
79
+ messageTypeId: number,
80
+ pbPayload: Uint8Array,
81
+ packetSrc?: number,
82
+ router?: number,
83
+ seq?: number
84
+ ): Uint8Array {
85
+ const payload = new Uint8Array(2 + pbPayload.length);
86
+ payload[0] = messageTypeId % 256;
87
+ payload[1] = Math.floor(messageTypeId / 256) % 256;
88
+ payload.set(pbPayload, 2);
89
+ return encodeFrame(payload, packetSrc, router, seq);
90
+ }
@@ -0,0 +1,98 @@
1
+ import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants';
2
+ import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF, PROTO_PRE_HEAD_SIZE } from './constants';
3
+ import { crc8 } from './crc8';
4
+
5
+ export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
6
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
7
+ const result = new Uint8Array(totalLength);
8
+ let offset = 0;
9
+ for (const arr of arrays) {
10
+ result.set(arr, offset);
11
+ offset += arr.length;
12
+ }
13
+ return result;
14
+ }
15
+
16
+ export class ProtocolV2FrameAssembler {
17
+ private buffer = new Uint8Array(0);
18
+
19
+ private readonly maxFrameBytes: number;
20
+
21
+ constructor(maxFrameBytes = PROTOCOL_V2_FRAME_MAX_BYTES) {
22
+ this.maxFrameBytes = maxFrameBytes;
23
+ }
24
+
25
+ reset() {
26
+ this.buffer = new Uint8Array(0);
27
+ }
28
+
29
+ push(chunk: Uint8Array): Uint8Array | undefined {
30
+ this.append(chunk);
31
+ return this.extractFrame();
32
+ }
33
+
34
+ /**
35
+ * Append a chunk (optional) and extract every complete frame currently
36
+ * buffered. Same validation/throw semantics as push(); push() stays
37
+ * backward compatible for callers that drain one frame at a time.
38
+ */
39
+ drain(chunk: Uint8Array = new Uint8Array(0)): Uint8Array[] {
40
+ this.append(chunk);
41
+ const frames: Uint8Array[] = [];
42
+ for (;;) {
43
+ const frame = this.extractFrame();
44
+ if (!frame) break;
45
+ frames.push(frame);
46
+ }
47
+ return frames;
48
+ }
49
+
50
+ private append(chunk: Uint8Array) {
51
+ if (chunk.length > 0) {
52
+ this.buffer = concatUint8Arrays([this.buffer, chunk]);
53
+ }
54
+ }
55
+
56
+ private extractFrame(): Uint8Array | undefined {
57
+ if (this.buffer.length < 3) return undefined;
58
+
59
+ if (this.buffer[0] !== PROTO_HEAD_SOF) {
60
+ this.reset();
61
+ throw new Error('Invalid Protocol V2 SOF');
62
+ }
63
+
64
+ const expectedLen = this.buffer[1] + this.buffer[2] * 256;
65
+ if (expectedLen < PROTO_HEAD_CRC_SIZE) {
66
+ // A declared length below the 8-byte frame overhead can never become a
67
+ // complete frame: without resetting, this poison prefix would stay in
68
+ // the buffer forever and deadlock the caller's drain loop.
69
+ this.reset();
70
+ throw new Error(`Protocol V2 frame length too small: ${expectedLen}`);
71
+ }
72
+ if (expectedLen > this.maxFrameBytes) {
73
+ this.reset();
74
+ throw new Error(`Protocol V2 frame too large: ${expectedLen}`);
75
+ }
76
+
77
+ if (this.buffer.length < PROTO_PRE_HEAD_SIZE) return undefined;
78
+
79
+ // Validate the pre-header CRC (byte 3 covers bytes 0-2) as soon as the
80
+ // first 4 bytes arrive, so a corrupted length field fails fast instead of
81
+ // waiting for bytes that will never come.
82
+ const expectedHeaderCrc = crc8(this.buffer, 3);
83
+ if (this.buffer[3] !== expectedHeaderCrc) {
84
+ this.reset();
85
+ throw new Error(
86
+ `Protocol V2 header CRC mismatch: expected 0x${expectedHeaderCrc
87
+ .toString(16)
88
+ .padStart(2, '0')}`
89
+ );
90
+ }
91
+
92
+ if (this.buffer.length < expectedLen) return undefined;
93
+
94
+ const frame = this.buffer.slice(0, expectedLen);
95
+ this.buffer = this.buffer.slice(expectedLen);
96
+ return frame;
97
+ }
98
+ }
@@ -0,0 +1,5 @@
1
+ export * from './constants';
2
+ export * from './crc8';
3
+ export * from './encode';
4
+ export * from './decode';
5
+ export * from './frame-assembler';
@@ -0,0 +1,162 @@
1
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
2
+ import { ProtocolV2Session, getErrorMessage } from './session';
3
+
4
+ import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
+ import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
6
+
7
+ export type ProtocolV2LinkErrorClassification = 'link-fatal' | 'recoverable';
8
+
9
+ export interface ProtocolV2LinkAdapter {
10
+ router: number;
11
+ maxFrameBytes?: number;
12
+ generation: number;
13
+ prepareCall(context: ProtocolV2CallContext): Promise<void> | void;
14
+ writeFrame(frame: Uint8Array, context: ProtocolV2CallContext): Promise<void>;
15
+ readFrame(context: ProtocolV2CallContext): Promise<Uint8Array>;
16
+ reset(reason: string): Promise<void> | void;
17
+ logger?: ProtocolV2SessionOptions['logger'];
18
+ logPrefix?: string;
19
+ createTimeoutError?: ProtocolV2SessionOptions['createTimeoutError'];
20
+ writeTimeoutMs?: number;
21
+ }
22
+
23
+ export type ProtocolV2LinkManagerOptions<Key> = {
24
+ getSchemas: () => ProtocolV2Schemas;
25
+ classifyError: (error: unknown) => ProtocolV2LinkErrorClassification;
26
+ onLinkInvalidated?: (key: Key, reason: string) => Promise<void> | void;
27
+ };
28
+
29
+ type ProtocolV2Link = {
30
+ adapter: ProtocolV2LinkAdapter;
31
+ session: ProtocolV2Session;
32
+ state: {
33
+ invalidatedReason?: string;
34
+ };
35
+ };
36
+
37
+ export class ProtocolV2LinkManager<Key> {
38
+ private readonly links = new Map<Key, ProtocolV2Link>();
39
+
40
+ private readonly sequences = new Map<Key, ProtocolV2SequenceCursor>();
41
+
42
+ private readonly callQueues = new Map<Key, Promise<unknown>>();
43
+
44
+ private readonly options: ProtocolV2LinkManagerOptions<Key>;
45
+
46
+ constructor(options: ProtocolV2LinkManagerOptions<Key>) {
47
+ this.options = options;
48
+ }
49
+
50
+ call(
51
+ key: Key,
52
+ createAdapter: () => ProtocolV2LinkAdapter,
53
+ name: string,
54
+ data: Record<string, unknown>,
55
+ options?: TransportCallOptions
56
+ ): Promise<MessageFromOneKey> {
57
+ const run = () => this.executeCall(key, createAdapter, name, data, options);
58
+ const previous = this.callQueues.get(key) ?? Promise.resolve();
59
+ const result = previous.then(run, run);
60
+ const queue = result.catch(() => undefined);
61
+ this.callQueues.set(key, queue);
62
+ result
63
+ .then(
64
+ () => this.clearSettledCallQueue(key, queue),
65
+ () => this.clearSettledCallQueue(key, queue)
66
+ )
67
+ .catch(() => undefined);
68
+ return result;
69
+ }
70
+
71
+ async invalidateLink(key: Key, reason: string): Promise<void> {
72
+ const link = this.links.get(key);
73
+ if (!link) return;
74
+
75
+ this.links.delete(key);
76
+ link.state.invalidatedReason = reason;
77
+ await link.adapter.reset(reason);
78
+ await this.options.onLinkInvalidated?.(key, reason);
79
+ }
80
+
81
+ async invalidateAllLinks(reason: string): Promise<void> {
82
+ await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason)));
83
+ }
84
+
85
+ async dispose(reason: string): Promise<void> {
86
+ await this.invalidateAllLinks(reason);
87
+ this.sequences.clear();
88
+ this.callQueues.clear();
89
+ }
90
+
91
+ private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
92
+ const existing = this.links.get(key);
93
+ if (existing) return existing;
94
+
95
+ const adapter = createAdapter();
96
+ const state: ProtocolV2Link['state'] = {};
97
+ const assertLinkActive = () => {
98
+ if (state.invalidatedReason) {
99
+ throw new Error(state.invalidatedReason);
100
+ }
101
+ };
102
+ let sequenceCursor = this.sequences.get(key);
103
+ if (!sequenceCursor) {
104
+ sequenceCursor = new ProtocolV2SequenceCursor();
105
+ this.sequences.set(key, sequenceCursor);
106
+ }
107
+ const session = new ProtocolV2Session({
108
+ schemas: this.options.getSchemas(),
109
+ router: adapter.router,
110
+ maxFrameBytes: adapter.maxFrameBytes,
111
+ generation: adapter.generation,
112
+ sequenceCursor,
113
+ prepareCall: async context => {
114
+ assertLinkActive();
115
+ await adapter.prepareCall(context);
116
+ assertLinkActive();
117
+ },
118
+ writeFrame: async (frame, context) => {
119
+ assertLinkActive();
120
+ await adapter.writeFrame(frame, context);
121
+ assertLinkActive();
122
+ },
123
+ readFrame: async context => {
124
+ assertLinkActive();
125
+ const frame = await adapter.readFrame(context);
126
+ assertLinkActive();
127
+ return frame;
128
+ },
129
+ logger: adapter.logger,
130
+ logPrefix: adapter.logPrefix,
131
+ createTimeoutError: adapter.createTimeoutError,
132
+ writeTimeoutMs: adapter.writeTimeoutMs,
133
+ });
134
+ const link = { adapter, session, state };
135
+ this.links.set(key, link);
136
+ return link;
137
+ }
138
+
139
+ private async executeCall(
140
+ key: Key,
141
+ createAdapter: () => ProtocolV2LinkAdapter,
142
+ name: string,
143
+ data: Record<string, unknown>,
144
+ options?: TransportCallOptions
145
+ ): Promise<MessageFromOneKey> {
146
+ try {
147
+ return await this.getOrCreateLink(key, createAdapter).session.call(name, data, options);
148
+ } catch (error) {
149
+ if (this.options.classifyError(error) === 'link-fatal') {
150
+ const errorMessage = getErrorMessage(error) || 'unknown error';
151
+ await this.invalidateLink(key, `Protocol V2 link-fatal error: ${errorMessage}`);
152
+ }
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ private clearSettledCallQueue(key: Key, queue: Promise<unknown>) {
158
+ if (this.callQueues.get(key) === queue) {
159
+ this.callQueues.delete(key);
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,10 @@
1
+ import { nextProtoSeq } from './encode';
2
+
3
+ export class ProtocolV2SequenceCursor {
4
+ private current = 0;
5
+
6
+ next(): number {
7
+ this.current = nextProtoSeq(this.current);
8
+ return this.current;
9
+ }
10
+ }