@onekeyfe/hd-core 1.2.0-alpha.100 → 1.2.0-alpha.102

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 (56) hide show
  1. package/__tests__/bridgeBinaryPayload.test.ts +173 -0
  2. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +2 -0
  3. package/__tests__/device-pool-state.test.ts +29 -0
  4. package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
  5. package/__tests__/get-device-state.test.ts +102 -13
  6. package/__tests__/protocol-v2-legacy-recovery.test.ts +29 -0
  7. package/__tests__/protocol-v2.test.ts +234 -1
  8. package/__tests__/refresh-device-state.test.ts +4 -1
  9. package/__tests__/search-devices.test.ts +2 -0
  10. package/__tests__/ton-sign-message.test.ts +84 -0
  11. package/dist/api/FirmwareUpdateV4.d.ts +3 -0
  12. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  13. package/dist/api/GetDeviceState.d.ts.map +1 -1
  14. package/dist/api/SearchDevices.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  16. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  17. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  18. package/dist/api/utils.d.ts.map +1 -1
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/protocolV2LegacyRecovery.d.ts +5 -0
  21. package/dist/core/protocolV2LegacyRecovery.d.ts.map +1 -0
  22. package/dist/device/Device.d.ts +9 -2
  23. package/dist/device/Device.d.ts.map +1 -1
  24. package/dist/device/DevicePool.d.ts +1 -1
  25. package/dist/device/DevicePool.d.ts.map +1 -1
  26. package/dist/index.d.ts +14 -4
  27. package/dist/index.js +263 -71
  28. package/dist/protocols/protocol-v2/features.d.ts +8 -0
  29. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  31. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  32. package/dist/topLevelInject.d.ts.map +1 -1
  33. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  34. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  35. package/dist/types/api/getDeviceState.d.ts +1 -0
  36. package/dist/types/api/getDeviceState.d.ts.map +1 -1
  37. package/dist/utils/bridgeBinaryPayload.d.ts +3 -0
  38. package/dist/utils/bridgeBinaryPayload.d.ts.map +1 -0
  39. package/package.json +4 -4
  40. package/src/api/FirmwareUpdateV4.ts +70 -23
  41. package/src/api/GetDeviceState.ts +1 -0
  42. package/src/api/SearchDevices.ts +2 -0
  43. package/src/api/firmware/FirmwareUpdatePlan.ts +37 -36
  44. package/src/api/firmware/protocolV2Release.ts +1 -0
  45. package/src/api/ton/TonSignMessage.ts +5 -3
  46. package/src/api/utils.ts +5 -2
  47. package/src/core/index.ts +4 -0
  48. package/src/core/protocolV2LegacyRecovery.ts +12 -0
  49. package/src/device/Device.ts +78 -20
  50. package/src/device/DevicePool.ts +17 -5
  51. package/src/protocols/protocol-v2/features.ts +10 -0
  52. package/src/protocols/protocol-v2/index.ts +2 -0
  53. package/src/topLevelInject.ts +4 -2
  54. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  55. package/src/types/api/getDeviceState.ts +2 -0
  56. package/src/utils/bridgeBinaryPayload.ts +137 -0
@@ -61,6 +61,7 @@ import {
61
61
  PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
62
62
  type ProtocolV2RuntimeMode,
63
63
  getProtocolV2RuntimeMode,
64
+ isLegacyProtocolV2ProtocolInfo,
64
65
  requestProtocolV2DeviceInfo,
65
66
  requestProtocolV2DeviceStatus,
66
67
  requestProtocolV2ProtocolInfo,
@@ -99,6 +100,8 @@ export type InitOptions = {
99
100
  protocolV2DeviceInfoTimeoutMs?: number;
100
101
  /** Refresh Protocol V2 runtime state before returning discovery results. */
101
102
  refreshRuntimeState?: boolean;
103
+ /** Recovery-only compatibility used by discovery and Protocol V2 firmware updates. */
104
+ allowLegacyProtocolV2ProtocolInfo?: boolean;
102
105
  /**
103
106
  * Protocol V1 Initialize response timeout override. Reboot-wait polling passes a
104
107
  * short value so an unanswered probe settles before the next poll tick.
@@ -977,10 +980,16 @@ export class Device extends EventEmitter {
977
980
  });
978
981
  // The default request excludes SE/hash data and therefore uses basic scope.
979
982
  // Full version and verification data require getDeviceState({ scope: 'firmware' }).
980
- const features = await this.probeProtocolV2RuntimeState(
981
- deviceInfo,
982
- options?.protocolV2DeviceInfoTimeoutMs
983
- );
983
+ const features = options?.allowLegacyProtocolV2ProtocolInfo
984
+ ? await this.probeProtocolV2RuntimeState(
985
+ deviceInfo,
986
+ options.protocolV2DeviceInfoTimeoutMs,
987
+ { allowLegacyProtocolV2ProtocolInfo: true }
988
+ )
989
+ : await this.probeProtocolV2RuntimeState(
990
+ deviceInfo,
991
+ options?.protocolV2DeviceInfoTimeoutMs
992
+ );
984
993
  Log.debug('Protocol V2 features:', features);
985
994
  } catch (error) {
986
995
  Log.error('Protocol V2 initialization failed:', error);
@@ -1017,7 +1026,13 @@ export class Device extends EventEmitter {
1017
1026
  commands: this.commands,
1018
1027
  request: getProtocolV2DeviceInfoRequest(),
1019
1028
  });
1020
- await this.probeProtocolV2RuntimeState(deviceInfo);
1029
+ if (params.allowLegacyProtocolV2ProtocolInfo) {
1030
+ await this.probeProtocolV2RuntimeState(deviceInfo, undefined, {
1031
+ allowLegacyProtocolV2ProtocolInfo: true,
1032
+ });
1033
+ } else {
1034
+ await this.probeProtocolV2RuntimeState(deviceInfo);
1035
+ }
1021
1036
  refreshedDeviceInfo = deviceInfo;
1022
1037
  initializedWithDeviceInfo = true;
1023
1038
  } else {
@@ -1053,7 +1068,16 @@ export class Device extends EventEmitter {
1053
1068
  }
1054
1069
 
1055
1070
  if (refresh.has('status') && !initializedWithDeviceInfo) {
1056
- await this.probeProtocolV2RuntimeState(refreshedDeviceInfo);
1071
+ const cachedMode = this.state?.status.mode;
1072
+ await this.probeProtocolV2RuntimeState(refreshedDeviceInfo, undefined, {
1073
+ // Loader firmware does not support DeviceStatusGet. Renegotiate ProtocolInfo
1074
+ // during an explicit refresh so a device rebooted into application firmware
1075
+ // can leave the cached loader state.
1076
+ forceRuntimeContextRefresh: cachedMode === 'bootloader' || cachedMode === 'romloader',
1077
+ ...(params.allowLegacyProtocolV2ProtocolInfo
1078
+ ? { allowLegacyProtocolV2ProtocolInfo: true }
1079
+ : {}),
1080
+ });
1057
1081
  }
1058
1082
 
1059
1083
  if (refresh.has('settings') && this.state?.status.mode === 'normal') {
@@ -1124,19 +1148,39 @@ export class Device extends EventEmitter {
1124
1148
  return this.features;
1125
1149
  }
1126
1150
 
1127
- async ensureProtocolV2RuntimeContext(timeoutMs?: number): Promise<ProtocolInfo> {
1151
+ async ensureProtocolV2RuntimeContext(
1152
+ timeoutMs?: number,
1153
+ options?: {
1154
+ forceRefresh?: boolean;
1155
+ allowLegacyProtocolV2ProtocolInfo?: boolean;
1156
+ }
1157
+ ): Promise<ProtocolInfo> {
1158
+ const assertProtocolInfoAllowed = (protocolInfo: ProtocolInfo) => {
1159
+ if (
1160
+ isLegacyProtocolV2ProtocolInfo(protocolInfo) &&
1161
+ options?.allowLegacyProtocolV2ProtocolInfo !== true
1162
+ ) {
1163
+ throw ERRORS.TypedError(
1164
+ HardwareErrorCode.DeviceInitializeFailed,
1165
+ 'Legacy Protocol V2 ProtocolInfo is supported only during device discovery and firmware update.'
1166
+ );
1167
+ }
1168
+ return protocolInfo;
1169
+ };
1128
1170
  const cachedProtocolInfo =
1129
- this.protocolV2RuntimeContext ??
1130
- (!this.protocolV2StateNeedsReload
1131
- ? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
1132
- : undefined);
1171
+ options?.forceRefresh === true
1172
+ ? undefined
1173
+ : this.protocolV2RuntimeContext ??
1174
+ (!this.protocolV2StateNeedsReload
1175
+ ? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
1176
+ : undefined);
1133
1177
  if (cachedProtocolInfo) {
1134
1178
  this.protocolV2RuntimeContext = cachedProtocolInfo;
1135
- return cachedProtocolInfo;
1179
+ return assertProtocolInfoAllowed(cachedProtocolInfo);
1136
1180
  }
1137
1181
 
1138
1182
  if (this.protocolV2RuntimeContextPromise) {
1139
- return this.protocolV2RuntimeContextPromise;
1183
+ return assertProtocolInfoAllowed(await this.protocolV2RuntimeContextPromise);
1140
1184
  }
1141
1185
 
1142
1186
  const requestToken = {};
@@ -1158,7 +1202,7 @@ export class Device extends EventEmitter {
1158
1202
  this.protocolV2RuntimeContextPromise = pendingRequest;
1159
1203
 
1160
1204
  try {
1161
- return await pendingRequest;
1205
+ return assertProtocolInfoAllowed(await pendingRequest);
1162
1206
  } finally {
1163
1207
  if (this.protocolV2RuntimeContextPromise === pendingRequest) {
1164
1208
  this.protocolV2RuntimeContextPromise = undefined;
@@ -1169,9 +1213,20 @@ export class Device extends EventEmitter {
1169
1213
  }
1170
1214
  }
1171
1215
 
1172
- async probeProtocolV2RuntimeState(deviceInfo?: ProtocolV2DeviceInfo, timeoutMs?: number) {
1173
- const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs);
1216
+ async probeProtocolV2RuntimeState(
1217
+ deviceInfo?: ProtocolV2DeviceInfo,
1218
+ timeoutMs?: number,
1219
+ options?: {
1220
+ forceRuntimeContextRefresh?: boolean;
1221
+ allowLegacyProtocolV2ProtocolInfo?: boolean;
1222
+ }
1223
+ ) {
1224
+ const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs, {
1225
+ forceRefresh: options?.forceRuntimeContextRefresh,
1226
+ allowLegacyProtocolV2ProtocolInfo: options?.allowLegacyProtocolV2ProtocolInfo,
1227
+ });
1174
1228
  const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
1229
+ const legacyProtocolInfo = isLegacyProtocolV2ProtocolInfo(protocolInfo);
1175
1230
  const runtimeDeviceInfo = deviceInfo ?? this.state?.raw?.protocolV2DeviceInfo;
1176
1231
  const protocolV2DeviceType = runtimeDeviceInfo
1177
1232
  ? resolveProtocolV2DeviceIdentity(runtimeDeviceInfo.hw?.Device_type).deviceType
@@ -1186,10 +1241,9 @@ export class Device extends EventEmitter {
1186
1241
  'Protocol V2 romloader mode is only supported for Pro2 and Neo.'
1187
1242
  );
1188
1243
  }
1189
- const deviceStatusSupported = supportsProtocolV2Message(
1190
- protocolInfo,
1191
- PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE
1192
- );
1244
+ const deviceStatusSupported =
1245
+ legacyProtocolInfo ||
1246
+ supportsProtocolV2Message(protocolInfo, PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE);
1193
1247
 
1194
1248
  if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
1195
1249
  return this.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
@@ -1491,6 +1545,10 @@ export class Device extends EventEmitter {
1491
1545
 
1492
1546
  async interruptionFromUser() {
1493
1547
  const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1548
+ // Cancellation often ends the transport session, especially during firmware
1549
+ // update reboot or exit. Invalidate V2 state before awaiting asynchronous
1550
+ // cancellation callbacks so the next request cannot reuse a stale runtime mode.
1551
+ this.markTransportDisconnected();
1494
1552
  await this.cancelableAction?.(error);
1495
1553
  await this.commands?.cancel();
1496
1554
 
@@ -182,7 +182,7 @@ export class DevicePool extends EventEmitter {
182
182
  try {
183
183
  await device.initialize(initOptions);
184
184
  if (initOptions?.refreshRuntimeState && device.isProtocolV2()) {
185
- await this._refreshProtocolV2DiscoveryState(device);
185
+ await this._refreshProtocolV2DiscoveryState(device, initOptions);
186
186
  }
187
187
  } finally {
188
188
  await device.release();
@@ -197,7 +197,7 @@ export class DevicePool extends EventEmitter {
197
197
  await device.run(
198
198
  async () => {
199
199
  try {
200
- await this._refreshProtocolV2DiscoveryState(device);
200
+ await this._refreshProtocolV2DiscoveryState(device, initOptions);
201
201
  } catch (error) {
202
202
  // Device.run releases after the callback; then propagate the actual read error.
203
203
  refreshError = error;
@@ -206,6 +206,9 @@ export class DevicePool extends EventEmitter {
206
206
  {
207
207
  connectProtocol: initOptions.connectProtocol,
208
208
  forceProtocolDetection: initOptions.forceProtocolDetection,
209
+ ...(initOptions.allowLegacyProtocolV2ProtocolInfo
210
+ ? { allowLegacyProtocolV2ProtocolInfo: true }
211
+ : {}),
209
212
  }
210
213
  );
211
214
  if (refreshError instanceof Error) throw refreshError;
@@ -217,10 +220,19 @@ export class DevicePool extends EventEmitter {
217
220
  * error; settings only supplies a label, so its failure retains the existing name.
218
221
  * Device.getDeviceState skips unsupported status/settings calls in loader mode.
219
222
  */
220
- static async _refreshProtocolV2DiscoveryState(device: Device) {
221
- await device.getDeviceState({ refreshSections: ['status'] });
223
+ static async _refreshProtocolV2DiscoveryState(device: Device, initOptions?: InitOptions) {
224
+ const stateReadOptions = initOptions?.allowLegacyProtocolV2ProtocolInfo
225
+ ? { allowLegacyProtocolV2ProtocolInfo: true as const }
226
+ : {};
227
+ await device.getDeviceState({
228
+ refreshSections: ['status'],
229
+ ...stateReadOptions,
230
+ });
222
231
  try {
223
- await device.getDeviceState({ refreshSections: ['settings'] });
232
+ await device.getDeviceState({
233
+ refreshSections: ['settings'],
234
+ ...stateReadOptions,
235
+ });
224
236
  } catch (error) {
225
237
  Log.debug('Unable to refresh Protocol V2 device label during discovery', error);
226
238
  }
@@ -56,6 +56,16 @@ export const getProtocolV2SeType = (se?: DeviceSEInfo): string | null =>
56
56
 
57
57
  export type ProtocolV2RuntimeMode = 'normal' | 'bootloader' | 'romloader';
58
58
 
59
+ export type ProtocolV2ProtocolInfo = ProtocolInfo & {
60
+ /** Present only when hd-transport decoded the pre-build-fingerprint wire layout. */
61
+ protobuf_definition?: string | null;
62
+ };
63
+
64
+ export const isLegacyProtocolV2ProtocolInfo = (
65
+ protocolInfo: ProtocolInfo
66
+ ): protocolInfo is ProtocolV2ProtocolInfo & { protobuf_definition: string | null } =>
67
+ Object.prototype.hasOwnProperty.call(protocolInfo, 'protobuf_definition');
68
+
59
69
  /**
60
70
  * Protocol V2 fingerprints use:
61
71
  * <binary>__<version>__<commit>__<PROD|DEV>__<DEBUG|RELEASE>
@@ -7,6 +7,7 @@ export {
7
7
  getProtocolV2RuntimeMode,
8
8
  getProtocolV2SeState,
9
9
  getProtocolV2SeType,
10
+ isLegacyProtocolV2ProtocolInfo,
10
11
  parseProtocolV2BuildFingerprint,
11
12
  requestProtocolV2ProtocolInfo,
12
13
  supportsProtocolV2Message,
@@ -18,6 +19,7 @@ export type {
18
19
  ProtocolV2SEInfo,
19
20
  ProtocolV2SeStateLabel,
20
21
  ProtocolV2RuntimeMode,
22
+ ProtocolV2ProtocolInfo,
21
23
  } from './features';
22
24
  export * from './firmware';
23
25
  export * from './walletSession';
@@ -2,6 +2,7 @@ import EventEmitter from 'events';
2
2
 
3
3
  import { createCoreApi, createProtocolAwareCall } from './inject';
4
4
  import { unregisterFirmwareUpdateHostBinding } from './api/firmware/FirmwareHostBinding';
5
+ import { encodeBridgeBinaryPayload } from './utils/bridgeBinaryPayload';
5
6
 
6
7
  import type { ConnectSettings } from './types/settings';
7
8
  import type { CoreApi } from './types/api';
@@ -16,9 +17,10 @@ const eventEmitter = new EventEmitter();
16
17
 
17
18
  export const topLevelInject = () => {
18
19
  let lowLevelApi: LowLevelCoreApi | undefined;
19
- const call = (params: any) => {
20
+ const call = async (params: any) => {
20
21
  if (!lowLevelApi) return Promise.resolve(undefined);
21
- return lowLevelApi.call(params);
22
+ const encodedParams = await encodeBridgeBinaryPayload(params);
23
+ return lowLevelApi.call(encodedParams as any);
22
24
  };
23
25
  const protocolAwareCall = createProtocolAwareCall(call);
24
26
  const api: CoreApi = {
@@ -20,8 +20,8 @@ export interface FirmwareUpdatePlanArtifact {
20
20
  url: string;
21
21
  container: 'raw' | 'zip';
22
22
  logicalName?: string;
23
- expectedSize: number;
24
- expectedSha256: string;
23
+ expectedSize?: number;
24
+ expectedSha256?: string;
25
25
  targetVersion?: string;
26
26
  }
27
27
 
@@ -11,6 +11,8 @@ export type GetDeviceStateParams = CommonParams & {
11
11
  export type DeviceStateReadOptions = {
12
12
  refreshSections?: DeviceStateSection[];
13
13
  includeRaw?: boolean;
14
+ /** Internal recovery-only compatibility used by discovery and firmware update flows. */
15
+ allowLegacyProtocolV2ProtocolInfo?: boolean;
14
16
  };
15
17
 
16
18
  export declare function getDeviceState(
@@ -0,0 +1,137 @@
1
+ import { Buffer } from 'buffer';
2
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+
4
+ // Top-level and low-level SDK instances can be separated by JSON-only hosts,
5
+ // including browser extension background/offscreen message bridges.
6
+ const BRIDGE_BINARY_PAYLOAD_MARKER = '__onekey_hd_bridge_binary_payload__';
7
+
8
+ type BridgeBinaryPayload = {
9
+ [BRIDGE_BINARY_PAYLOAD_MARKER]: 1;
10
+ data: string;
11
+ type: 'array-buffer' | 'uint8-array';
12
+ };
13
+
14
+ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
15
+
16
+ const invalidBinaryPayload = (message: string) =>
17
+ ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter, message);
18
+
19
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
21
+ const prototype = Object.getPrototypeOf(value);
22
+ return prototype === Object.prototype || prototype === null;
23
+ }
24
+
25
+ function isArrayBufferValue(value: unknown): value is ArrayBuffer {
26
+ return Boolean(
27
+ typeof ArrayBuffer !== 'undefined' &&
28
+ (value instanceof ArrayBuffer ||
29
+ Object.prototype.toString.call(value) === '[object ArrayBuffer]')
30
+ );
31
+ }
32
+
33
+ function isBlobValue(value: unknown): value is Blob {
34
+ return Boolean(
35
+ typeof Blob !== 'undefined' &&
36
+ (value instanceof Blob || Object.prototype.toString.call(value) === '[object Blob]') &&
37
+ typeof (value as Blob).arrayBuffer === 'function'
38
+ );
39
+ }
40
+
41
+ function readBinaryPayload(value: unknown): BridgeBinaryPayload | undefined {
42
+ if (!isPlainObject(value) || !(BRIDGE_BINARY_PAYLOAD_MARKER in value)) return undefined;
43
+ const payload = value as Partial<BridgeBinaryPayload>;
44
+ if (
45
+ payload[BRIDGE_BINARY_PAYLOAD_MARKER] !== 1 ||
46
+ (payload.type !== 'array-buffer' && payload.type !== 'uint8-array') ||
47
+ typeof payload.data !== 'string'
48
+ ) {
49
+ throw invalidBinaryPayload('Invalid bridge binary payload');
50
+ }
51
+ return payload as BridgeBinaryPayload;
52
+ }
53
+
54
+ function bytesToPayload(bytes: Uint8Array, type: BridgeBinaryPayload['type']): BridgeBinaryPayload {
55
+ return {
56
+ [BRIDGE_BINARY_PAYLOAD_MARKER]: 1,
57
+ data: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64'),
58
+ type,
59
+ };
60
+ }
61
+
62
+ function payloadToBytes(payload: BridgeBinaryPayload): Uint8Array {
63
+ if (payload.data.length % 4 !== 0 || !BASE64_PATTERN.test(payload.data)) {
64
+ throw invalidBinaryPayload('Invalid bridge binary payload data');
65
+ }
66
+ const decoded = Buffer.from(payload.data, 'base64');
67
+ if (decoded.toString('base64') !== payload.data) {
68
+ throw invalidBinaryPayload('Invalid bridge binary payload data');
69
+ }
70
+ return Uint8Array.from(decoded);
71
+ }
72
+
73
+ async function encodeValue(value: unknown, seen: WeakSet<object>): Promise<unknown> {
74
+ if (isArrayBufferValue(value)) {
75
+ return bytesToPayload(new Uint8Array(value), 'array-buffer');
76
+ }
77
+ if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(value)) {
78
+ return bytesToPayload(
79
+ new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
80
+ 'uint8-array'
81
+ );
82
+ }
83
+ if (isBlobValue(value)) {
84
+ return bytesToPayload(new Uint8Array(await value.arrayBuffer()), 'uint8-array');
85
+ }
86
+
87
+ const encodedPayload = readBinaryPayload(value);
88
+ if (encodedPayload) return encodedPayload;
89
+ if (Array.isArray(value)) {
90
+ if (seen.has(value)) throw invalidBinaryPayload('Circular bridge payload');
91
+ seen.add(value);
92
+ try {
93
+ const encoded: unknown[] = [];
94
+ for (const item of value) {
95
+ encoded.push(await encodeValue(item, seen));
96
+ }
97
+ return encoded.some((item, index) => item !== value[index]) ? encoded : value;
98
+ } finally {
99
+ seen.delete(value);
100
+ }
101
+ }
102
+ if (!isPlainObject(value)) return value;
103
+ if (seen.has(value)) throw invalidBinaryPayload('Circular bridge payload');
104
+ seen.add(value);
105
+ try {
106
+ const entries: [string, unknown][] = [];
107
+ for (const [key, item] of Object.entries(value)) {
108
+ entries.push([key, await encodeValue(item, seen)]);
109
+ }
110
+ return entries.some(([key, item]) => item !== value[key]) ? Object.fromEntries(entries) : value;
111
+ } finally {
112
+ seen.delete(value);
113
+ }
114
+ }
115
+
116
+ function decodeValue(value: unknown): unknown {
117
+ const payload = readBinaryPayload(value);
118
+ if (payload) {
119
+ const bytes = payloadToBytes(payload);
120
+ return payload.type === 'array-buffer' ? bytes.buffer : bytes;
121
+ }
122
+ if (Array.isArray(value)) {
123
+ const decoded = value.map(decodeValue);
124
+ return decoded.some((item, index) => item !== value[index]) ? decoded : value;
125
+ }
126
+ if (!isPlainObject(value)) return value;
127
+ const entries = Object.entries(value).map(([key, item]) => [key, decodeValue(item)] as const);
128
+ return entries.some(([key, item]) => item !== value[key]) ? Object.fromEntries(entries) : value;
129
+ }
130
+
131
+ export async function encodeBridgeBinaryPayload(value: unknown): Promise<unknown> {
132
+ return encodeValue(value, new WeakSet());
133
+ }
134
+
135
+ export function decodeBridgeBinaryPayload(value: unknown): unknown {
136
+ return decodeValue(value);
137
+ }