@onekeyfe/hd-transport 1.1.28 → 1.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/__tests__/build-receive.test.js +6 -8
- package/__tests__/decode-features.test.js +3 -2
- package/__tests__/messages.test.js +8 -0
- package/__tests__/protocol-v2.test.js +754 -0
- package/dist/constants.d.ts +14 -5
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +905 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +811 -84
- package/dist/protocols/index.d.ts +45 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/v1/decode.d.ts +11 -0
- package/dist/protocols/v1/decode.d.ts.map +1 -0
- package/dist/protocols/v1/encode.d.ts +11 -0
- package/dist/protocols/v1/encode.d.ts.map +1 -0
- package/dist/protocols/v1/index.d.ts +5 -0
- package/dist/protocols/v1/index.d.ts.map +1 -0
- package/dist/protocols/v1/packets.d.ts +7 -0
- package/dist/protocols/v1/packets.d.ts.map +1 -0
- package/dist/{serialization → protocols/v1}/receive.d.ts +1 -1
- package/dist/protocols/v1/receive.d.ts.map +1 -0
- package/dist/protocols/v2/constants.d.ts +7 -0
- package/dist/protocols/v2/constants.d.ts.map +1 -0
- package/dist/protocols/v2/crc8.d.ts +3 -0
- package/dist/protocols/v2/crc8.d.ts.map +1 -0
- package/dist/protocols/v2/debug.d.ts +13 -0
- package/dist/protocols/v2/debug.d.ts.map +1 -0
- package/dist/protocols/v2/decode.d.ts +8 -0
- package/dist/protocols/v2/decode.d.ts.map +1 -0
- package/dist/protocols/v2/encode.d.ts +5 -0
- package/dist/protocols/v2/encode.d.ts.map +1 -0
- package/dist/protocols/v2/frame-assembler.d.ts +12 -0
- package/dist/protocols/v2/frame-assembler.d.ts.map +1 -0
- package/dist/protocols/v2/index.d.ts +7 -0
- package/dist/protocols/v2/index.d.ts.map +1 -0
- package/dist/protocols/v2/session.d.ts +50 -0
- package/dist/protocols/v2/session.d.ts.map +1 -0
- package/dist/serialization/index.d.ts +6 -3
- package/dist/serialization/index.d.ts.map +1 -1
- package/dist/serialization/protobuf/decode.d.ts.map +1 -1
- package/dist/serialization/protobuf/messages.d.ts +1 -1
- package/dist/serialization/protobuf/messages.d.ts.map +1 -1
- package/dist/types/messages.d.ts +441 -11
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/transport.d.ts +14 -2
- package/dist/types/transport.d.ts.map +1 -1
- package/dist/utils/logBlockCommand.d.ts.map +1 -1
- package/messages-protocol-v2.json +13375 -0
- package/package.json +3 -3
- package/scripts/protobuf-build.sh +314 -20
- package/scripts/protobuf-patches/TxInputType.js +1 -0
- package/scripts/protobuf-patches/index.js +2 -0
- package/scripts/protobuf-types.js +224 -18
- package/src/constants.ts +42 -6
- package/src/index.ts +39 -11
- package/src/protocols/index.ts +144 -0
- package/src/{serialization/protocol → protocols/v1}/decode.ts +4 -4
- package/src/{serialization/protocol → protocols/v1}/encode.ts +18 -13
- package/src/protocols/v1/index.ts +4 -0
- package/src/protocols/v1/packets.ts +53 -0
- package/src/{serialization → protocols/v1}/receive.ts +5 -5
- package/src/protocols/v2/constants.ts +6 -0
- package/src/protocols/v2/crc8.ts +34 -0
- package/src/protocols/v2/debug.ts +26 -0
- package/src/protocols/v2/decode.ts +92 -0
- package/src/protocols/v2/encode.ts +116 -0
- package/src/protocols/v2/frame-assembler.ts +98 -0
- package/src/protocols/v2/index.ts +6 -0
- package/src/protocols/v2/session.ts +429 -0
- package/src/serialization/index.ts +6 -5
- package/src/serialization/protobuf/decode.ts +7 -0
- package/src/serialization/protobuf/messages.ts +8 -4
- package/src/types/messages.ts +579 -13
- package/src/types/transport.ts +26 -2
- package/src/utils/logBlockCommand.ts +9 -1
- package/dist/serialization/protocol/decode.d.ts +0 -11
- package/dist/serialization/protocol/decode.d.ts.map +0 -1
- package/dist/serialization/protocol/encode.d.ts +0 -11
- package/dist/serialization/protocol/encode.d.ts.map +0 -1
- package/dist/serialization/protocol/index.d.ts +0 -3
- package/dist/serialization/protocol/index.d.ts.map +0 -1
- package/dist/serialization/receive.d.ts.map +0 -1
- package/dist/serialization/send.d.ts +0 -7
- package/dist/serialization/send.d.ts.map +0 -1
- package/src/serialization/protocol/index.ts +0 -2
- package/src/serialization/send.ts +0 -58
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
|
|
2
|
+
import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
|
|
3
|
+
import { nextProtoSeq } from './encode';
|
|
4
|
+
import { ProtocolV2 } from '..';
|
|
5
|
+
import * as check from '../../utils/highlevel-checks';
|
|
6
|
+
import { LogBlockCommand } from '../../utils/logBlockCommand';
|
|
7
|
+
|
|
8
|
+
import type { Root } from 'protobufjs/light';
|
|
9
|
+
import type { MessageFromOneKey } from '../../types';
|
|
10
|
+
|
|
11
|
+
export type ProtocolV2Schemas = {
|
|
12
|
+
protocolV1: Root;
|
|
13
|
+
protocolV2: Root;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type ProtocolLogger = {
|
|
17
|
+
debug?: (...args: any[]) => void;
|
|
18
|
+
error?: (...args: any[]) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ProtocolV2SessionOptions = {
|
|
22
|
+
schemas: ProtocolV2Schemas;
|
|
23
|
+
router: number;
|
|
24
|
+
packetSrc?: number;
|
|
25
|
+
writeFrame: (frame: Uint8Array) => Promise<void>;
|
|
26
|
+
readFrame: () => Promise<Uint8Array>;
|
|
27
|
+
logger?: ProtocolLogger;
|
|
28
|
+
logPrefix?: string;
|
|
29
|
+
createTimeoutError?: (name: string, timeoutMs: number) => Error;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ProtocolV2CallOptions = {
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
expectedTypes?: string[];
|
|
35
|
+
intermediateTypes?: string[];
|
|
36
|
+
onIntermediateResponse?: (response: MessageFromOneKey) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { concatUint8Arrays, ProtocolV2FrameAssembler };
|
|
40
|
+
|
|
41
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
42
|
+
const clean = hex.replace(/\s+/g, '');
|
|
43
|
+
if (clean.length % 2 !== 0) {
|
|
44
|
+
throw new Error(`Invalid hex string: odd length ${clean.length}`);
|
|
45
|
+
}
|
|
46
|
+
if (!/^[0-9a-fA-F]*$/.test(clean)) {
|
|
47
|
+
throw new Error('Invalid hex string: contains non-hex characters');
|
|
48
|
+
}
|
|
49
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
50
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
51
|
+
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
52
|
+
}
|
|
53
|
+
return bytes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function bytesToHex(bytes: Uint8Array): string {
|
|
57
|
+
return Array.from(bytes)
|
|
58
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
59
|
+
.join('');
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
|
|
72
|
+
...LogBlockCommand,
|
|
73
|
+
'FilesystemFileRead',
|
|
74
|
+
'FileRead',
|
|
75
|
+
'EmmcFileRead',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function shouldReduceProtocolV2Debug(name: string) {
|
|
79
|
+
return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
|
|
80
|
+
}
|
|
81
|
+
|
|
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
|
+
const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
|
|
179
|
+
'Failure',
|
|
180
|
+
'ButtonRequest',
|
|
181
|
+
'EntropyRequest',
|
|
182
|
+
'PinMatrixRequest',
|
|
183
|
+
'PassphraseRequest',
|
|
184
|
+
'Deprecated_PassphraseStateRequest',
|
|
185
|
+
'WordRequest',
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
function isExpectedTerminalResponse(
|
|
189
|
+
response: MessageFromOneKey,
|
|
190
|
+
expectedTypes: string[] | undefined
|
|
191
|
+
) {
|
|
192
|
+
if (!expectedTypes || expectedTypes.length === 0) return true;
|
|
193
|
+
return expectedTypes.includes(response.type) || COMMON_TERMINAL_RESPONSE_TYPES.has(response.type);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getErrorMessage(error: unknown) {
|
|
197
|
+
if (!error) return '';
|
|
198
|
+
if (typeof error === 'string') return error;
|
|
199
|
+
if (typeof error === 'object' && 'message' in error) {
|
|
200
|
+
const { message } = error as { message?: unknown };
|
|
201
|
+
return typeof message === 'string' ? message : String(message ?? '');
|
|
202
|
+
}
|
|
203
|
+
return String(error);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function withProtocolTimeout<T>(
|
|
207
|
+
promise: Promise<T>,
|
|
208
|
+
timeoutMs: number | undefined,
|
|
209
|
+
createTimeoutError: () => Error,
|
|
210
|
+
onTimeout?: () => void
|
|
211
|
+
): Promise<T> {
|
|
212
|
+
if (!timeoutMs) return promise;
|
|
213
|
+
|
|
214
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
215
|
+
try {
|
|
216
|
+
return await Promise.race([
|
|
217
|
+
promise,
|
|
218
|
+
new Promise<never>((_, reject) => {
|
|
219
|
+
timer = setTimeout(() => {
|
|
220
|
+
// Give the caller a chance to cancel the underlying work; a plain
|
|
221
|
+
// Promise.race rejection leaves the raced promise running.
|
|
222
|
+
onTimeout?.();
|
|
223
|
+
reject(createTimeoutError());
|
|
224
|
+
}, timeoutMs);
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
227
|
+
} finally {
|
|
228
|
+
if (timer) clearTimeout(timer);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
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;
|
|
237
|
+
|
|
238
|
+
export class ProtocolV2Session {
|
|
239
|
+
private readonly options: ProtocolV2SessionOptions;
|
|
240
|
+
|
|
241
|
+
// Serializes call() invocations: responses are matched only by type, so two
|
|
242
|
+
// in-flight calls on the same session would steal each other's responses.
|
|
243
|
+
private pendingCall: Promise<unknown> = Promise.resolve();
|
|
244
|
+
|
|
245
|
+
// Per-session sequence counter (1-255, wraps skipping 0).
|
|
246
|
+
private protoSeq = 0;
|
|
247
|
+
|
|
248
|
+
constructor(options: ProtocolV2SessionOptions) {
|
|
249
|
+
this.options = options;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
call(
|
|
253
|
+
name: string,
|
|
254
|
+
data: Record<string, unknown>,
|
|
255
|
+
callOptions: ProtocolV2CallOptions = {}
|
|
256
|
+
): Promise<MessageFromOneKey> {
|
|
257
|
+
const run = () => this.executeCall(name, data, callOptions);
|
|
258
|
+
const result = this.pendingCall.then(run, run);
|
|
259
|
+
// Keep the chain alive even when a call fails; errors still propagate to
|
|
260
|
+
// the per-call promise returned below.
|
|
261
|
+
this.pendingCall = result.catch(() => undefined);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async executeCall(
|
|
266
|
+
name: string,
|
|
267
|
+
data: Record<string, unknown>,
|
|
268
|
+
callOptions: ProtocolV2CallOptions
|
|
269
|
+
): Promise<MessageFromOneKey> {
|
|
270
|
+
const {
|
|
271
|
+
schemas,
|
|
272
|
+
router,
|
|
273
|
+
packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
|
|
274
|
+
writeFrame,
|
|
275
|
+
readFrame,
|
|
276
|
+
logger,
|
|
277
|
+
logPrefix = 'ProtocolV2',
|
|
278
|
+
createTimeoutError,
|
|
279
|
+
} = this.options;
|
|
280
|
+
|
|
281
|
+
const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
|
|
282
|
+
this.protoSeq = nextProtoSeq(this.protoSeq);
|
|
283
|
+
const frame = ProtocolV2.encodeFrame(schemas, name, data, {
|
|
284
|
+
packetSrc,
|
|
285
|
+
router,
|
|
286
|
+
seq: this.protoSeq,
|
|
287
|
+
logger: shouldReduceDebug ? undefined : logger,
|
|
288
|
+
logPrefix,
|
|
289
|
+
context: `tx:${name}`,
|
|
290
|
+
});
|
|
291
|
+
const expectedSeq = frame[6];
|
|
292
|
+
|
|
293
|
+
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)}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Lenient watchdog on the write phase only — see
|
|
306
|
+
// PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
|
|
307
|
+
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
|
+
)
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Cancellation flag for the read loop: when the response timeout fires,
|
|
317
|
+
// Promise.race alone would leave this loop running as a zombie that keeps
|
|
318
|
+
// consuming frames meant for the next call. The timeout callback flips the
|
|
319
|
+
// flag so the loop exits and discards any late frame.
|
|
320
|
+
const cancellation = { cancelled: false };
|
|
321
|
+
|
|
322
|
+
const readResponse = async (): Promise<MessageFromOneKey> => {
|
|
323
|
+
// Some Protocol V2 operations emit progress notifications before the
|
|
324
|
+
// terminal response. Consume those frames here so callers still see a
|
|
325
|
+
// request/terminal-response shaped API.
|
|
326
|
+
while (!cancellation.cancelled) {
|
|
327
|
+
const rxFrame = await readFrame();
|
|
328
|
+
if (cancellation.cancelled) {
|
|
329
|
+
// Timed out while waiting: drop the late frame and stop reading.
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
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
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Only reachable after cancellation; the outer promise has already been
|
|
373
|
+
// rejected by the timeout, so this rejection is consumed by the race.
|
|
374
|
+
throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
return withProtocolTimeout(
|
|
378
|
+
readResponse(),
|
|
379
|
+
callOptions.timeoutMs,
|
|
380
|
+
() =>
|
|
381
|
+
createTimeoutError
|
|
382
|
+
? createTimeoutError(name, callOptions.timeoutMs ?? 0)
|
|
383
|
+
: new Error(`Protocol V2 response timeout after ${callOptions.timeoutMs}ms for ${name}`),
|
|
384
|
+
() => {
|
|
385
|
+
cancellation.cancelled = true;
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function probeProtocolV2({
|
|
392
|
+
call,
|
|
393
|
+
timeoutMs,
|
|
394
|
+
logger,
|
|
395
|
+
logPrefix = 'ProtocolV2',
|
|
396
|
+
onBeforeProbe,
|
|
397
|
+
onProbeFailed,
|
|
398
|
+
}: {
|
|
399
|
+
call: (
|
|
400
|
+
name: string,
|
|
401
|
+
data: Record<string, unknown>,
|
|
402
|
+
options?: ProtocolV2CallOptions
|
|
403
|
+
) => Promise<MessageFromOneKey>;
|
|
404
|
+
timeoutMs: number;
|
|
405
|
+
logger?: ProtocolLogger;
|
|
406
|
+
logPrefix?: string;
|
|
407
|
+
onBeforeProbe?: () => Promise<void> | void;
|
|
408
|
+
onProbeFailed?: (error: unknown) => Promise<void> | void;
|
|
409
|
+
}) {
|
|
410
|
+
let probeError: unknown;
|
|
411
|
+
try {
|
|
412
|
+
await onBeforeProbe?.();
|
|
413
|
+
const response = await call(
|
|
414
|
+
'Ping',
|
|
415
|
+
{ message: 'protocol-v2-probe' },
|
|
416
|
+
{ timeoutMs, expectedTypes: ['Success'] }
|
|
417
|
+
);
|
|
418
|
+
if (response.type === 'Success') {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
probeError = new Error(`unexpected response type ${response.type}`);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
probeError = error;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
logger?.debug?.(`[${logPrefix}] Protocol V2 probe failed:`, getErrorMessage(probeError));
|
|
427
|
+
await onProbeFailed?.(probeError);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { parseConfigure } from './protobuf';
|
|
2
2
|
|
|
3
|
-
export * from './send';
|
|
4
|
-
export * from './receive';
|
|
5
|
-
|
|
6
|
-
export * as decodeProtocol from './protocol/decode';
|
|
7
|
-
|
|
8
3
|
export { parseConfigure };
|
|
4
|
+
export { createMessageFromName, createMessageFromType } from './protobuf/messages';
|
|
5
|
+
export { encode as encodeProtobuf } from './protobuf/encode';
|
|
6
|
+
export { decode as decodeProtobuf } from './protobuf/decode';
|
|
7
|
+
export { PROTOCOL_V2_SYS_MESSAGE_THRESHOLD, ProtocolV1, ProtocolV2 } from '../protocols';
|
|
8
|
+
export * as protocolV1 from '../protocols/v1';
|
|
9
|
+
export * as protocolV2 from '../protocols/v2';
|
|
@@ -40,6 +40,13 @@ function messageToJSON(Message: Message<Record<string, unknown>>, fields: Type['
|
|
|
40
40
|
// @ts-ignore
|
|
41
41
|
const value = message[key];
|
|
42
42
|
|
|
43
|
+
if (value == null) {
|
|
44
|
+
if (field.optional) {
|
|
45
|
+
res[key] = null;
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
/* istanbul ignore else */
|
|
44
51
|
if (field.repeated) {
|
|
45
52
|
/* istanbul ignore else */
|
|
@@ -11,15 +11,19 @@ export function parseConfigure(data: protobuf.INamespace) {
|
|
|
11
11
|
export const createMessageFromName = (messages: protobuf.Root, name: string) => {
|
|
12
12
|
const Message = messages.lookupType(name);
|
|
13
13
|
const MessageType = messages.lookupEnum('MessageType');
|
|
14
|
-
let
|
|
14
|
+
let messageTypeId = MessageType.values[`MessageType_${name}`];
|
|
15
15
|
|
|
16
|
-
if (
|
|
17
|
-
|
|
16
|
+
if (messageTypeId == null && Message.options) {
|
|
17
|
+
messageTypeId = Message.options['(wire_type)'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!Number.isInteger(messageTypeId)) {
|
|
21
|
+
throw new Error(`MessageType for "${name}" is not defined in protobuf schema`);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
return {
|
|
21
25
|
Message,
|
|
22
|
-
|
|
26
|
+
messageTypeId,
|
|
23
27
|
};
|
|
24
28
|
};
|
|
25
29
|
|