@onekeyfe/hardware-cli 1.2.0-alpha.17 → 1.2.0-alpha.170
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/dist/cli.d.ts +66 -0
- package/dist/cli.js +191 -284
- package/dist/deviceSelection.d.ts +1 -1
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +10 -2
- package/dist/sdk.js +17 -8
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.js +26 -39
- package/package.json +7 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +1 -3
- package/src/__tests__/firmware-update-v4-command.test.ts +83 -1
- package/src/__tests__/noble-ble-plugin.test.ts +147 -2
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +1 -0
- package/src/cli.ts +222 -339
- package/src/deviceSelection.ts +1 -1
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +18 -10
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +34 -45
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
|
|
4
|
-
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
5
|
+
import { EDeviceType, isSameOnekeyBleName } from '@onekeyfe/hd-shared';
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
resolveBatchGetAddress,
|
|
@@ -11,65 +12,30 @@ import {
|
|
|
11
12
|
resolveSignTransaction,
|
|
12
13
|
} from './chains';
|
|
13
14
|
import { selectSearchDevice } from './deviceSelection';
|
|
15
|
+
import { getCanonicalDeviceState, getCompatibleFeatures } from './deviceStateCommands';
|
|
14
16
|
import { createSDK, disposeSDK } from './sdk';
|
|
15
|
-
import {
|
|
16
|
-
clearSessionFromKeychain,
|
|
17
|
-
preloadSessionFromKeychain,
|
|
18
|
-
saveSessionToKeychain,
|
|
19
|
-
} from './session';
|
|
17
|
+
import { clearSessionFromKeychain, preloadSessionFromKeychain } from './session';
|
|
20
18
|
|
|
21
19
|
import type {
|
|
20
|
+
DeviceStateScope,
|
|
22
21
|
EthereumSignTypedDataMessage,
|
|
23
22
|
EthereumSignTypedDataTypes,
|
|
24
23
|
Features,
|
|
25
|
-
IDeviceType,
|
|
26
24
|
SearchDevice,
|
|
27
25
|
} from '@onekeyfe/hd-core';
|
|
28
26
|
|
|
29
27
|
/** SearchDevice enriched with features fetched after discovery */
|
|
30
28
|
type EnrichedSearchDevice = SearchDevice & { features?: Features };
|
|
31
29
|
|
|
32
|
-
function extractPassphraseSession(payload: unknown): {
|
|
33
|
-
passphraseState?: string;
|
|
34
|
-
sessionId?: string;
|
|
35
|
-
} {
|
|
36
|
-
if (typeof payload === 'string') {
|
|
37
|
-
return { passphraseState: payload };
|
|
38
|
-
}
|
|
39
|
-
if (!payload || typeof payload !== 'object') {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const statePayload = payload as {
|
|
44
|
-
passphrase_state?: unknown;
|
|
45
|
-
passphraseState?: unknown;
|
|
46
|
-
session_id?: unknown;
|
|
47
|
-
sessionId?: unknown;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
let passphraseState: string | undefined;
|
|
51
|
-
if (typeof statePayload.passphrase_state === 'string') {
|
|
52
|
-
passphraseState = statePayload.passphrase_state;
|
|
53
|
-
} else if (typeof statePayload.passphraseState === 'string') {
|
|
54
|
-
passphraseState = statePayload.passphraseState;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let sessionId: string | undefined;
|
|
58
|
-
if (typeof statePayload.session_id === 'string') {
|
|
59
|
-
sessionId = statePayload.session_id;
|
|
60
|
-
} else if (typeof statePayload.sessionId === 'string') {
|
|
61
|
-
sessionId = statePayload.sessionId;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { passphraseState, sessionId };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
30
|
const program = new Command();
|
|
31
|
+
const { version: cliVersion } = JSON.parse(
|
|
32
|
+
readFileSync(resolve(__dirname, '../package.json'), 'utf8')
|
|
33
|
+
) as { version: string };
|
|
68
34
|
|
|
69
35
|
program
|
|
70
36
|
.name('onekey-hw')
|
|
71
37
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
72
|
-
.version(
|
|
38
|
+
.version(cliVersion);
|
|
73
39
|
|
|
74
40
|
// ============================================================
|
|
75
41
|
// Global Options
|
|
@@ -95,28 +61,6 @@ program
|
|
|
95
61
|
.action(() =>
|
|
96
62
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
97
63
|
const result = await sdk.searchDevices();
|
|
98
|
-
|
|
99
|
-
// USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
|
|
100
|
-
if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
|
|
101
|
-
for (const device of result.payload as EnrichedSearchDevice[]) {
|
|
102
|
-
if (device.connectId) {
|
|
103
|
-
try {
|
|
104
|
-
const features = await sdk.getFeatures(device.connectId);
|
|
105
|
-
if (features?.success && features.payload) {
|
|
106
|
-
device.features = features.payload;
|
|
107
|
-
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
108
|
-
const devType = features.payload.deviceType?.toLowerCase();
|
|
109
|
-
if (devType) {
|
|
110
|
-
device.deviceType = devType as IDeviceType;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// Features fetch failed — device may need PIN, continue with basic info
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
64
|
outputResult(globalOpts, result);
|
|
121
65
|
})
|
|
122
66
|
);
|
|
@@ -126,24 +70,28 @@ program
|
|
|
126
70
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
127
71
|
.action(() =>
|
|
128
72
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
73
|
+
const result = await getCompatibleFeatures(sdk, globalOpts.connectId);
|
|
74
|
+
outputResult(globalOpts, result);
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('get-state')
|
|
80
|
+
.description('Get canonical device state for Protocol V1 and Protocol V2 devices')
|
|
81
|
+
.option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
|
|
82
|
+
.action((opts: { scope: string }) =>
|
|
83
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
84
|
+
const supportedScopes: DeviceStateScope[] = ['runtime', 'settings', 'firmware'];
|
|
85
|
+
if (!supportedScopes.includes(opts.scope as DeviceStateScope)) {
|
|
86
|
+
const error = new Error(`Unsupported device state scope: ${opts.scope}`);
|
|
87
|
+
(error as Error & { code?: string }).code = 'INVALID_DEVICE_STATE_SCOPE';
|
|
88
|
+
throw error;
|
|
145
89
|
}
|
|
146
|
-
const result = await
|
|
90
|
+
const result = await getCanonicalDeviceState(
|
|
91
|
+
sdk,
|
|
92
|
+
globalOpts.connectId,
|
|
93
|
+
opts.scope as DeviceStateScope
|
|
94
|
+
);
|
|
147
95
|
outputResult(globalOpts, result);
|
|
148
96
|
})
|
|
149
97
|
);
|
|
@@ -151,18 +99,12 @@ program
|
|
|
151
99
|
program
|
|
152
100
|
.command('upload-wallpaper')
|
|
153
101
|
.description('Upload and activate a Pro2 wallpaper')
|
|
154
|
-
.requiredOption('--
|
|
102
|
+
.requiredOption('--jpeg <path>', '604x1024 JPEG file')
|
|
155
103
|
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
156
104
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
157
105
|
.action(opts =>
|
|
158
106
|
runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
159
|
-
const
|
|
160
|
-
const expectedBytes = 604 * 1024 * 4;
|
|
161
|
-
if (rgba.byteLength !== expectedBytes) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
107
|
+
const jpegBase64 = readFileSync(opts.jpeg).toString('base64');
|
|
166
108
|
|
|
167
109
|
let transferStartedAt: number | undefined;
|
|
168
110
|
let transferEndedAt: number | undefined;
|
|
@@ -211,9 +153,7 @@ program
|
|
|
211
153
|
try {
|
|
212
154
|
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
213
155
|
...params,
|
|
214
|
-
|
|
215
|
-
height: 1024,
|
|
216
|
-
rgba,
|
|
156
|
+
jpegBase64,
|
|
217
157
|
fileName: opts.fileName,
|
|
218
158
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
219
159
|
});
|
|
@@ -680,10 +620,6 @@ program
|
|
|
680
620
|
.command('firmware-update-v4')
|
|
681
621
|
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
682
622
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
683
|
-
.option(
|
|
684
|
-
'--resource-bundle <spec...>',
|
|
685
|
-
'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
|
|
686
|
-
)
|
|
687
623
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
688
624
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
689
625
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -693,6 +629,7 @@ program
|
|
|
693
629
|
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
694
630
|
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
695
631
|
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
632
|
+
.option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
|
|
696
633
|
.option('--forced-update-res', 'Force resource update')
|
|
697
634
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
698
635
|
.action(opts =>
|
|
@@ -848,7 +785,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
848
785
|
|
|
849
786
|
sessionCmd
|
|
850
787
|
.command('connect')
|
|
851
|
-
.description('Connect device and
|
|
788
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
852
789
|
.action(() =>
|
|
853
790
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
854
791
|
// 1. Search for device
|
|
@@ -860,7 +797,17 @@ sessionCmd
|
|
|
860
797
|
});
|
|
861
798
|
return;
|
|
862
799
|
}
|
|
863
|
-
const device =
|
|
800
|
+
const device = selectSearchDevice(
|
|
801
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
802
|
+
globalOpts.connectId
|
|
803
|
+
);
|
|
804
|
+
if (!device) {
|
|
805
|
+
outputResult(globalOpts, {
|
|
806
|
+
success: false,
|
|
807
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
864
811
|
const connectId = device.connectId || globalOpts.connectId;
|
|
865
812
|
|
|
866
813
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -871,62 +818,35 @@ sessionCmd
|
|
|
871
818
|
await unlockWithRetry(sdk, connectId);
|
|
872
819
|
}
|
|
873
820
|
|
|
874
|
-
// 3.
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
useEmptyPassphrase: false,
|
|
821
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
822
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
823
|
+
mode: 'select-hidden',
|
|
878
824
|
});
|
|
879
|
-
if (!
|
|
880
|
-
outputResult(globalOpts,
|
|
825
|
+
if (!sessionResult.success) {
|
|
826
|
+
outputResult(globalOpts, sessionResult);
|
|
881
827
|
return;
|
|
882
828
|
}
|
|
883
|
-
|
|
884
|
-
psResult.payload
|
|
885
|
-
);
|
|
886
|
-
if (!passphraseState) {
|
|
829
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
887
830
|
outputResult(globalOpts, {
|
|
888
831
|
success: false,
|
|
889
|
-
payload: { error: '
|
|
832
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
890
833
|
});
|
|
891
834
|
return;
|
|
892
835
|
}
|
|
836
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
893
837
|
|
|
894
838
|
// 4. Get address to verify + extract deviceId
|
|
895
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
839
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
896
840
|
path: "m/44'/60'/0'/0/0",
|
|
897
841
|
showOnOneKey: false,
|
|
898
842
|
passphraseState,
|
|
899
843
|
});
|
|
900
844
|
|
|
901
|
-
// 5. Fetch the now-active session_id via getFeatures.
|
|
902
|
-
//
|
|
903
|
-
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
904
|
-
// connectStateChange guard (core/index.ts) would see the payload's
|
|
905
|
-
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
906
|
-
// and call Initialize again with no passphrase_state / no session_id.
|
|
907
|
-
// That Initialize resets the device to the standard wallet and returns
|
|
908
|
-
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
909
|
-
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
910
|
-
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
911
|
-
const featResult = await sdk.getFeatures(connectId, {
|
|
912
|
-
passphraseState,
|
|
913
|
-
skipPassphraseCheck: true,
|
|
914
|
-
});
|
|
915
|
-
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
916
|
-
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
917
|
-
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
918
|
-
|
|
919
|
-
// 6. Save to keychain
|
|
920
|
-
if (passphraseState && deviceId && sessionId) {
|
|
921
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
845
|
outputResult(globalOpts, {
|
|
925
846
|
success: true,
|
|
926
847
|
payload: {
|
|
927
848
|
passphraseState,
|
|
928
849
|
deviceId,
|
|
929
|
-
...(sessionId ? { sessionId } : {}),
|
|
930
850
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
931
851
|
},
|
|
932
852
|
});
|
|
@@ -939,8 +859,10 @@ sessionCmd
|
|
|
939
859
|
.action(() =>
|
|
940
860
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
941
861
|
const searchResult = await sdk.searchDevices();
|
|
942
|
-
const device =
|
|
943
|
-
(searchResult?.payload as
|
|
862
|
+
const device = selectSearchDevice(
|
|
863
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
864
|
+
globalOpts.connectId
|
|
865
|
+
);
|
|
944
866
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
945
867
|
if (deviceId) {
|
|
946
868
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -1035,12 +957,12 @@ async function unlockWithRetry(
|
|
|
1035
957
|
* Prepare passphrase session before SDK calls.
|
|
1036
958
|
*
|
|
1037
959
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
1038
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
1039
|
-
* 3. Keychain miss →
|
|
960
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
961
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
1040
962
|
*
|
|
1041
963
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
1042
964
|
*/
|
|
1043
|
-
async function prepareSession(
|
|
965
|
+
export async function prepareSession(
|
|
1044
966
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
1045
967
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1046
968
|
globalOpts: Record<string, any>
|
|
@@ -1051,7 +973,7 @@ async function prepareSession(
|
|
|
1051
973
|
}
|
|
1052
974
|
|
|
1053
975
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
1054
|
-
//
|
|
976
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
1055
977
|
// catch block, which renders them as structured `{ success: false,
|
|
1056
978
|
// payload: { error, code } }` output instead of silently falling through
|
|
1057
979
|
// to a confusing downstream error 112 / 114.
|
|
@@ -1070,6 +992,7 @@ async function prepareSession(
|
|
|
1070
992
|
searchResult.payload as Array<{
|
|
1071
993
|
connectId?: string;
|
|
1072
994
|
deviceId?: string;
|
|
995
|
+
deviceType?: string;
|
|
1073
996
|
features?: {
|
|
1074
997
|
deviceId?: string | null;
|
|
1075
998
|
deviceType?: string;
|
|
@@ -1088,6 +1011,7 @@ async function prepareSession(
|
|
|
1088
1011
|
const selectedDevice = device as {
|
|
1089
1012
|
connectId?: string;
|
|
1090
1013
|
deviceId?: string;
|
|
1014
|
+
deviceType?: string;
|
|
1091
1015
|
features?: {
|
|
1092
1016
|
deviceId?: string | null;
|
|
1093
1017
|
deviceType?: string;
|
|
@@ -1105,7 +1029,8 @@ async function prepareSession(
|
|
|
1105
1029
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
1106
1030
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
1107
1031
|
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1108
|
-
let deviceType =
|
|
1032
|
+
let deviceType =
|
|
1033
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1109
1034
|
let unlocked = selectedDevice.features?.unlocked;
|
|
1110
1035
|
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
1111
1036
|
|
|
@@ -1146,7 +1071,7 @@ async function prepareSession(
|
|
|
1146
1071
|
return undefined;
|
|
1147
1072
|
}
|
|
1148
1073
|
|
|
1149
|
-
// ── Step 5: Try keychain session reuse
|
|
1074
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
1150
1075
|
// Only attempt if device was already unlocked — locking invalidates
|
|
1151
1076
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
1152
1077
|
if (!wasLocked && deviceId) {
|
|
@@ -1157,40 +1082,19 @@ async function prepareSession(
|
|
|
1157
1082
|
}
|
|
1158
1083
|
}
|
|
1159
1084
|
|
|
1160
|
-
// ── Step 6: Keychain miss →
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
useEmptyPassphrase: false,
|
|
1085
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1086
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1087
|
+
mode: 'select-hidden',
|
|
1164
1088
|
});
|
|
1165
1089
|
|
|
1166
|
-
if (
|
|
1167
|
-
|
|
1168
|
-
psResult.payload
|
|
1169
|
-
);
|
|
1170
|
-
if (!passphraseState) {
|
|
1090
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1091
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
1171
1092
|
return undefined;
|
|
1172
1093
|
}
|
|
1094
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1095
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
1173
1096
|
globalOpts.passphraseState = passphraseState;
|
|
1174
1097
|
|
|
1175
|
-
// Save session to keychain for next invocation.
|
|
1176
|
-
//
|
|
1177
|
-
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
1178
|
-
// Initialize would be re-run without passphrase_state, resetting the
|
|
1179
|
-
// device to the standard wallet and returning a mismatched session_id.
|
|
1180
|
-
// See the matching comment in `session connect`.
|
|
1181
|
-
if (deviceId) {
|
|
1182
|
-
const featAfter = await sdk.getFeatures(connectId, {
|
|
1183
|
-
passphraseState,
|
|
1184
|
-
skipPassphraseCheck: true,
|
|
1185
|
-
});
|
|
1186
|
-
const sessionId =
|
|
1187
|
-
passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
1188
|
-
if (sessionId) {
|
|
1189
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
1190
|
-
await preloadSessionFromKeychain(deviceId);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
1098
|
return passphraseState;
|
|
1195
1099
|
}
|
|
1196
1100
|
return undefined;
|
|
@@ -1207,8 +1111,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1207
1111
|
) {
|
|
1208
1112
|
process.exitCode = 1;
|
|
1209
1113
|
}
|
|
1210
|
-
// No process.exit here — runCommand()
|
|
1211
|
-
//
|
|
1114
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1115
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1212
1116
|
}
|
|
1213
1117
|
|
|
1214
1118
|
/**
|
|
@@ -1220,7 +1124,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1220
1124
|
* 3. run the handler (which calls outputResult on success)
|
|
1221
1125
|
* 4. report uncaught errors as a structured failure result
|
|
1222
1126
|
* 5. dispose SDK
|
|
1223
|
-
* 6.
|
|
1127
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1224
1128
|
*
|
|
1225
1129
|
* This fixes three previous bugs:
|
|
1226
1130
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1272,9 +1176,7 @@ async function runCommand(
|
|
|
1272
1176
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1273
1177
|
await disposeSDK();
|
|
1274
1178
|
}
|
|
1275
|
-
//
|
|
1276
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1277
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1179
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1278
1180
|
}
|
|
1279
1181
|
|
|
1280
1182
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1315,52 +1217,37 @@ async function resolveLegacyFirmwareConnectId(
|
|
|
1315
1217
|
}
|
|
1316
1218
|
|
|
1317
1219
|
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1318
|
-
const
|
|
1319
|
-
const matches =
|
|
1320
|
-
? devices.filter(
|
|
1220
|
+
const requestedName = deviceName?.trim();
|
|
1221
|
+
const matches = requestedName
|
|
1222
|
+
? devices.filter(
|
|
1223
|
+
device =>
|
|
1224
|
+
isSameOnekeyBleName(device.name, requestedName) ||
|
|
1225
|
+
device.name?.trim().toLowerCase() === requestedName.toLowerCase()
|
|
1226
|
+
)
|
|
1321
1227
|
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1322
1228
|
|
|
1323
1229
|
if (matches.length === 0) {
|
|
1324
1230
|
throw new Error(
|
|
1325
|
-
|
|
1231
|
+
requestedName
|
|
1232
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1233
|
+
: 'No Classic/Pure BLE device found'
|
|
1326
1234
|
);
|
|
1327
1235
|
}
|
|
1328
1236
|
if (matches.length > 1) {
|
|
1329
1237
|
throw new Error(
|
|
1330
|
-
|
|
1238
|
+
requestedName
|
|
1331
1239
|
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1332
1240
|
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1333
1241
|
);
|
|
1334
1242
|
}
|
|
1335
1243
|
|
|
1336
|
-
const connectId = matches
|
|
1337
|
-
if (!connectId) throw new Error(`BLE device has no connect ID: ${
|
|
1244
|
+
const [{ connectId, name }] = matches;
|
|
1245
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1338
1246
|
return connectId;
|
|
1339
1247
|
}
|
|
1340
1248
|
|
|
1341
|
-
function parseResourceBundleParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
|
|
1342
|
-
const sep = spec.indexOf(':');
|
|
1343
|
-
if (sep <= 0 || sep === spec.length - 1) {
|
|
1344
|
-
throw new Error(
|
|
1345
|
-
`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
const localPath = spec.slice(0, sep);
|
|
1349
|
-
const devicePath = spec.slice(sep + 1);
|
|
1350
|
-
if (!devicePath.startsWith('vol')) {
|
|
1351
|
-
throw new Error(
|
|
1352
|
-
`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
return {
|
|
1356
|
-
binary: readBinaryParam(localPath),
|
|
1357
|
-
devicePath,
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
1249
|
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1362
1250
|
return [
|
|
1363
|
-
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1364
1251
|
params.bootloaderBinary,
|
|
1365
1252
|
params.applicationP1Binary,
|
|
1366
1253
|
params.applicationP2Binary,
|
|
@@ -1429,9 +1316,7 @@ export function buildWallpaperUploadMetrics({
|
|
|
1429
1316
|
transferredBytes,
|
|
1430
1317
|
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1431
1318
|
transferKiBPerSecond:
|
|
1432
|
-
elapsedMs > 0
|
|
1433
|
-
? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2))
|
|
1434
|
-
: null,
|
|
1319
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1435
1320
|
lastProgress,
|
|
1436
1321
|
};
|
|
1437
1322
|
}
|
|
@@ -1532,7 +1417,7 @@ function buildFirmwareUpdateV4Metrics({
|
|
|
1532
1417
|
};
|
|
1533
1418
|
}
|
|
1534
1419
|
|
|
1535
|
-
async function runFirmwareUpdateV4WithRetry({
|
|
1420
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1536
1421
|
sdk,
|
|
1537
1422
|
globalOpts,
|
|
1538
1423
|
params,
|
|
@@ -1546,152 +1431,147 @@ async function runFirmwareUpdateV4WithRetry({
|
|
|
1546
1431
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1547
1432
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1548
1433
|
let currentSdk = sdk;
|
|
1549
|
-
let lastResult: unknown;
|
|
1550
1434
|
let retried = false;
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const connectId =
|
|
1565
|
-
retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1566
|
-
? undefined
|
|
1567
|
-
: globalOpts.connectId;
|
|
1568
|
-
|
|
1569
|
-
const onUiEvent = (message: unknown) => {
|
|
1570
|
-
if (!message || typeof message !== 'object') return;
|
|
1571
|
-
const messageType = (message as { type?: string }).type;
|
|
1572
|
-
const payload = getFirmwareUpdatePayload(message);
|
|
1573
|
-
|
|
1574
|
-
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1575
|
-
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1576
|
-
if (typeof tipMessage === 'string') {
|
|
1577
|
-
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1578
|
-
}
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1583
|
-
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1584
|
-
process.stderr.write(
|
|
1585
|
-
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1586
|
-
);
|
|
1587
|
-
return;
|
|
1435
|
+
let attempt = 1;
|
|
1436
|
+
let { connectId } = globalOpts;
|
|
1437
|
+
|
|
1438
|
+
if (globalOpts.transport === 'usb') {
|
|
1439
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1440
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1441
|
+
scope: 'runtime',
|
|
1442
|
+
connectProtocol: 'V2',
|
|
1443
|
+
retryCount: 0,
|
|
1444
|
+
});
|
|
1445
|
+
if (isSuccessResult(probeResult)) break;
|
|
1446
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1447
|
+
return probeResult;
|
|
1588
1448
|
}
|
|
1589
1449
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1603
|
-
});
|
|
1604
|
-
if (progress >= 100) {
|
|
1605
|
-
transferEndedAt ??= Date.now();
|
|
1606
|
-
}
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1450
|
+
retried = true;
|
|
1451
|
+
process.stderr.write(
|
|
1452
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1453
|
+
);
|
|
1454
|
+
await disposeSDK();
|
|
1455
|
+
await new Promise(resolve => {
|
|
1456
|
+
setTimeout(resolve, 3000);
|
|
1457
|
+
});
|
|
1458
|
+
currentSdk = await createSDK(globalOpts);
|
|
1459
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1609
1462
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1463
|
+
let progressEvents = 0;
|
|
1464
|
+
let lastProgress = -1;
|
|
1465
|
+
let transferStartedAt: number | undefined;
|
|
1466
|
+
let transferEndedAt: number | undefined;
|
|
1467
|
+
let installProgressEvents = 0;
|
|
1468
|
+
let lastInstallProgress = -1;
|
|
1469
|
+
let installStartedAt: number | undefined;
|
|
1470
|
+
let installEndedAt: number | undefined;
|
|
1471
|
+
let lastPrintedTransferProgress = -10;
|
|
1472
|
+
let lastPrintedInstallProgress = -10;
|
|
1473
|
+
const totalStartedAt = Date.now();
|
|
1474
|
+
|
|
1475
|
+
const onUiEvent = (message: unknown) => {
|
|
1476
|
+
if (!message || typeof message !== 'object') return;
|
|
1477
|
+
const messageType = (message as { type?: string }).type;
|
|
1478
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1479
|
+
|
|
1480
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1481
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1482
|
+
if (typeof tipMessage === 'string') {
|
|
1483
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1623
1484
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1627
|
-
try {
|
|
1628
|
-
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1629
|
-
} finally {
|
|
1630
|
-
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1631
|
-
}
|
|
1632
|
-
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1633
|
-
installEndedAt = Date.now();
|
|
1485
|
+
return;
|
|
1634
1486
|
}
|
|
1635
1487
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
transferEndedAt,
|
|
1643
|
-
installStartedAt,
|
|
1644
|
-
installEndedAt,
|
|
1645
|
-
progressEvents,
|
|
1646
|
-
lastProgress,
|
|
1647
|
-
installProgressEvents,
|
|
1648
|
-
lastInstallProgress,
|
|
1649
|
-
retried,
|
|
1650
|
-
});
|
|
1651
|
-
|
|
1652
|
-
if (lastResult && typeof lastResult === 'object') {
|
|
1653
|
-
const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
|
|
1654
|
-
string,
|
|
1655
|
-
unknown
|
|
1656
|
-
>;
|
|
1657
|
-
lastResult = {
|
|
1658
|
-
...(lastResult as Record<string, unknown>),
|
|
1659
|
-
payload: {
|
|
1660
|
-
...payload,
|
|
1661
|
-
metrics,
|
|
1662
|
-
},
|
|
1663
|
-
};
|
|
1488
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1489
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1490
|
+
process.stderr.write(
|
|
1491
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1492
|
+
);
|
|
1493
|
+
return;
|
|
1664
1494
|
}
|
|
1665
1495
|
|
|
1666
|
-
if (
|
|
1667
|
-
|
|
1496
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1497
|
+
const progress = Number(payload.progress);
|
|
1498
|
+
if (!Number.isFinite(progress)) return;
|
|
1499
|
+
|
|
1500
|
+
if (payload.progressType === 'transferData') {
|
|
1501
|
+
progressEvents += 1;
|
|
1502
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1503
|
+
transferStartedAt ??= Date.now();
|
|
1504
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1505
|
+
progressType: 'transfer',
|
|
1506
|
+
progress,
|
|
1507
|
+
payload,
|
|
1508
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1509
|
+
});
|
|
1510
|
+
if (progress >= 100) {
|
|
1511
|
+
transferEndedAt ??= Date.now();
|
|
1512
|
+
}
|
|
1513
|
+
return;
|
|
1668
1514
|
}
|
|
1669
1515
|
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1516
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1517
|
+
installProgressEvents += 1;
|
|
1518
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1519
|
+
installStartedAt ??= Date.now();
|
|
1520
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1521
|
+
progressType: 'install',
|
|
1522
|
+
progress,
|
|
1523
|
+
payload,
|
|
1524
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1525
|
+
});
|
|
1526
|
+
if (progress >= 100) {
|
|
1527
|
+
installEndedAt ??= Date.now();
|
|
1528
|
+
}
|
|
1676
1529
|
}
|
|
1530
|
+
};
|
|
1677
1531
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
);
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1532
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1533
|
+
let result: unknown;
|
|
1534
|
+
try {
|
|
1535
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1536
|
+
} finally {
|
|
1537
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1538
|
+
}
|
|
1539
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1540
|
+
installEndedAt = Date.now();
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1544
|
+
attempt,
|
|
1545
|
+
maxAttempts,
|
|
1546
|
+
totalBytes,
|
|
1547
|
+
totalStartedAt,
|
|
1548
|
+
transferStartedAt,
|
|
1549
|
+
transferEndedAt,
|
|
1550
|
+
installStartedAt,
|
|
1551
|
+
installEndedAt,
|
|
1552
|
+
progressEvents,
|
|
1553
|
+
lastProgress,
|
|
1554
|
+
installProgressEvents,
|
|
1555
|
+
lastInstallProgress,
|
|
1556
|
+
retried,
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
if (result && typeof result === 'object') {
|
|
1560
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1561
|
+
return {
|
|
1562
|
+
...(result as Record<string, unknown>),
|
|
1563
|
+
payload: {
|
|
1564
|
+
...payload,
|
|
1565
|
+
metrics,
|
|
1566
|
+
},
|
|
1567
|
+
};
|
|
1687
1568
|
}
|
|
1688
1569
|
|
|
1689
|
-
return
|
|
1570
|
+
return result;
|
|
1690
1571
|
}
|
|
1691
1572
|
|
|
1692
1573
|
function buildFirmwareUpdateV4Params(opts: {
|
|
1693
1574
|
chunkSize?: string;
|
|
1694
|
-
resourceBundle?: string[];
|
|
1695
1575
|
romloader?: string;
|
|
1696
1576
|
bootloader?: string;
|
|
1697
1577
|
applicationP1?: string;
|
|
@@ -1701,6 +1581,7 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1701
1581
|
se02?: string;
|
|
1702
1582
|
se03?: string;
|
|
1703
1583
|
se04?: string;
|
|
1584
|
+
resourceArchive?: string;
|
|
1704
1585
|
forcedUpdateRes?: boolean;
|
|
1705
1586
|
}) {
|
|
1706
1587
|
const params = {
|
|
@@ -1708,7 +1589,6 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1708
1589
|
connectProtocol: 'V2' as const,
|
|
1709
1590
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1710
1591
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1711
|
-
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1712
1592
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1713
1593
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1714
1594
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1718,10 +1598,10 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1718
1598
|
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1719
1599
|
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1720
1600
|
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1601
|
+
resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
|
|
1721
1602
|
};
|
|
1722
1603
|
|
|
1723
1604
|
const hasPayload = [
|
|
1724
|
-
params.resourceBundleFiles,
|
|
1725
1605
|
params.romloaderBinary,
|
|
1726
1606
|
params.bootloaderBinary,
|
|
1727
1607
|
params.applicationP1Binary,
|
|
@@ -1731,10 +1611,13 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1731
1611
|
params.se02Binary,
|
|
1732
1612
|
params.se03Binary,
|
|
1733
1613
|
params.se04Binary,
|
|
1614
|
+
params.resourceArchiveBinary,
|
|
1734
1615
|
].some(Boolean);
|
|
1735
1616
|
|
|
1736
1617
|
if (!hasPayload) {
|
|
1737
|
-
const err = new Error(
|
|
1618
|
+
const err = new Error(
|
|
1619
|
+
'firmware-update-v4 requires at least one firmware binary or resource archive path'
|
|
1620
|
+
);
|
|
1738
1621
|
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1739
1622
|
throw err;
|
|
1740
1623
|
}
|