@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.
- package/__tests__/bridgeBinaryPayload.test.ts +173 -0
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +2 -0
- package/__tests__/device-pool-state.test.ts +29 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
- package/__tests__/get-device-state.test.ts +102 -13
- package/__tests__/protocol-v2-legacy-recovery.test.ts +29 -0
- package/__tests__/protocol-v2.test.ts +234 -1
- package/__tests__/refresh-device-state.test.ts +4 -1
- package/__tests__/search-devices.test.ts +2 -0
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/FirmwareUpdateV4.d.ts +3 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetDeviceState.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/api/utils.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/protocolV2LegacyRecovery.d.ts +5 -0
- package/dist/core/protocolV2LegacyRecovery.d.ts.map +1 -0
- package/dist/device/Device.d.ts +9 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.js +263 -71
- package/dist/protocols/protocol-v2/features.d.ts +8 -0
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
- package/dist/types/api/getDeviceState.d.ts +1 -0
- package/dist/types/api/getDeviceState.d.ts.map +1 -1
- package/dist/utils/bridgeBinaryPayload.d.ts +3 -0
- package/dist/utils/bridgeBinaryPayload.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +70 -23
- package/src/api/GetDeviceState.ts +1 -0
- package/src/api/SearchDevices.ts +2 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +37 -36
- package/src/api/firmware/protocolV2Release.ts +1 -0
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/api/utils.ts +5 -2
- package/src/core/index.ts +4 -0
- package/src/core/protocolV2LegacyRecovery.ts +12 -0
- package/src/device/Device.ts +78 -20
- package/src/device/DevicePool.ts +17 -5
- package/src/protocols/protocol-v2/features.ts +10 -0
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/topLevelInject.ts +4 -2
- package/src/types/api/firmwareUpdatePlan.ts +2 -2
- package/src/types/api/getDeviceState.ts +2 -0
- package/src/utils/bridgeBinaryPayload.ts +137 -0
package/src/device/Device.ts
CHANGED
|
@@ -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 =
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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(
|
|
1173
|
-
|
|
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 =
|
|
1190
|
-
|
|
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
|
|
package/src/device/DevicePool.ts
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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';
|
package/src/topLevelInject.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
24
|
-
expectedSha256
|
|
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
|
+
}
|