@onekeyfe/hd-core 1.2.0-alpha.58 → 1.2.0-alpha.59

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 (91) hide show
  1. package/__tests__/DeviceCommands.test.ts +5 -15
  2. package/__tests__/device-pool-state.test.ts +22 -0
  3. package/__tests__/device-state-mapper.test.ts +12 -0
  4. package/__tests__/device-utils.test.ts +48 -1
  5. package/__tests__/deviceSettings.test.ts +10 -0
  6. package/__tests__/deviceUploadNft.test.ts +4 -28
  7. package/__tests__/kaspa-use-tweak-pro2.test.ts +2 -2
  8. package/__tests__/protocol-v2-resources.test.ts +28 -13
  9. package/__tests__/protocol-v2-unlock-policy.test.ts +34 -0
  10. package/__tests__/protocol-v2.test.ts +159 -137
  11. package/__tests__/tron-sign-message-init.test.ts +2 -2
  12. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  13. package/dist/api/FirmwareUpdateV4.d.ts +4 -3
  14. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  15. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  16. package/dist/api/kaspa/KaspaGetAddress.d.ts +1 -1
  17. package/dist/api/kaspa/KaspaSignTransaction.d.ts +1 -1
  18. package/dist/api/polkadot/PolkadotGetAddress.d.ts +1 -1
  19. package/dist/api/polkadot/networks.d.ts +1 -1
  20. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  21. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  22. package/dist/api/solana/SolSignMessage.d.ts +1 -1
  23. package/dist/api/solana/SolSignOffchainMessage.d.ts +1 -1
  24. package/dist/api/solana/SolSignTransaction.d.ts +2 -2
  25. package/dist/api/sui/SuiGetAddress.d.ts +1 -1
  26. package/dist/api/sui/SuiGetPublicKey.d.ts +1 -1
  27. package/dist/api/sui/SuiSignMessage.d.ts +1 -1
  28. package/dist/api/sui/SuiSignTransaction.d.ts +1 -1
  29. package/dist/api/ton/TonGetAddress.d.ts +1 -1
  30. package/dist/api/ton/TonSignMessage.d.ts +1 -1
  31. package/dist/api/ton/TonSignProof.d.ts +1 -1
  32. package/dist/api/tron/TronSignMessage.d.ts +2 -2
  33. package/dist/api/tron/TronSignTransaction.d.ts +1 -1
  34. package/dist/data-manager/DataManager.d.ts +7 -5
  35. package/dist/data-manager/DataManager.d.ts.map +1 -1
  36. package/dist/device/Device.d.ts.map +1 -1
  37. package/dist/device/DeviceCommands.d.ts.map +1 -1
  38. package/dist/device/DevicePool.d.ts.map +1 -1
  39. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -1
  40. package/dist/index.d.ts +44 -34
  41. package/dist/index.js +231 -168
  42. package/dist/protocols/protocol-v2/resources.d.ts +2 -2
  43. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  44. package/dist/types/api/checkAllFirmwareRelease.d.ts +1 -1
  45. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  46. package/dist/types/api/firmwareUpdate.d.ts +3 -2
  47. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  48. package/dist/types/device.d.ts +2 -2
  49. package/dist/types/device.d.ts.map +1 -1
  50. package/dist/types/settings.d.ts +22 -14
  51. package/dist/types/settings.d.ts.map +1 -1
  52. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  53. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  54. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  55. package/dist/utils/deviceSettings.d.ts.map +1 -1
  56. package/package.json +4 -4
  57. package/src/api/CheckAllFirmwareRelease.ts +7 -5
  58. package/src/api/FirmwareUpdateV4.ts +115 -102
  59. package/src/api/OpenWalletSession.ts +1 -0
  60. package/src/api/kaspa/KaspaGetAddress.ts +1 -1
  61. package/src/api/kaspa/KaspaSignTransaction.ts +1 -1
  62. package/src/api/polkadot/networks.ts +3 -3
  63. package/src/api/protocol-v2/DeviceUploadNft.ts +7 -12
  64. package/src/api/protocol-v2/helpers.ts +0 -1
  65. package/src/api/solana/SolSignMessage.ts +1 -1
  66. package/src/api/solana/SolSignOffchainMessage.ts +1 -1
  67. package/src/api/solana/SolSignTransaction.ts +2 -2
  68. package/src/api/sui/SuiGetAddress.ts +1 -1
  69. package/src/api/sui/SuiGetPublicKey.ts +1 -1
  70. package/src/api/sui/SuiSignMessage.ts +1 -1
  71. package/src/api/sui/SuiSignTransaction.ts +1 -1
  72. package/src/api/ton/TonGetAddress.ts +1 -1
  73. package/src/api/ton/TonSignMessage.ts +1 -1
  74. package/src/api/ton/TonSignProof.ts +1 -1
  75. package/src/api/tron/TronSignMessage.ts +2 -2
  76. package/src/api/tron/TronSignTransaction.ts +1 -1
  77. package/src/data-manager/DataManager.ts +28 -9
  78. package/src/device/Device.ts +15 -6
  79. package/src/device/DeviceCommands.ts +13 -10
  80. package/src/device/DevicePool.ts +17 -8
  81. package/src/deviceProfile/buildDeviceFeatures.ts +2 -2
  82. package/src/deviceProfile/protocolV2DeviceIdentity.ts +3 -0
  83. package/src/protocols/protocol-v2/resources.ts +44 -13
  84. package/src/types/api/checkAllFirmwareRelease.ts +1 -1
  85. package/src/types/api/firmwareUpdate.ts +5 -5
  86. package/src/types/device.ts +12 -3
  87. package/src/types/settings.ts +30 -21
  88. package/src/utils/deviceFeaturesCompat.ts +5 -0
  89. package/src/utils/deviceFeaturesUtils.ts +6 -3
  90. package/src/utils/deviceInfoUtils.ts +2 -0
  91. package/src/utils/deviceSettings.ts +3 -0
@@ -69,7 +69,7 @@ function getFirmwareTypeFromField(firmwareField: IFirmwareField): EFirmwareType
69
69
 
70
70
  export default class DataManager {
71
71
  static deviceMap: DeviceTypeMap & {
72
- [k: string]: DeviceTypeMap[keyof DeviceTypeMap] | undefined;
72
+ [k: string]: NonNullable<DeviceTypeMap[keyof DeviceTypeMap]> | undefined;
73
73
  } = {
74
74
  [EDeviceType.Classic]: {
75
75
  firmware: [],
@@ -95,6 +95,10 @@ export default class DataManager {
95
95
  firmware: [],
96
96
  ble: [],
97
97
  },
98
+ [EDeviceType.Neo]: {
99
+ firmware: [],
100
+ ble: [],
101
+ },
98
102
  [EDeviceType.ClassicPure]: {
99
103
  firmware: [],
100
104
  ble: [],
@@ -359,7 +363,7 @@ export default class DataManager {
359
363
 
360
364
  private static enrichFirmwareReleaseInfo(
361
365
  deviceData: DeviceTypeMap[keyof DeviceTypeMap] | undefined
362
- ): DeviceTypeMap[keyof DeviceTypeMap] {
366
+ ): NonNullable<DeviceTypeMap[keyof DeviceTypeMap]> {
363
367
  // Safety check: return default structure if input is undefined/null
364
368
  if (!deviceData || typeof deviceData !== 'object') {
365
369
  return {
@@ -451,10 +455,15 @@ export default class DataManager {
451
455
  // 3. Apply config if available
452
456
  if (data) {
453
457
  let pro2Resources: IProtocolV2Resources | undefined;
458
+ let neoResources: IProtocolV2Resources | undefined;
459
+ this.protocolV2ResourcesConfigError = undefined;
454
460
  try {
455
461
  pro2Resources = parseProtocolV2Resources(
456
462
  (data.pro2 as { resources?: unknown } | undefined)?.resources
457
463
  );
464
+ neoResources = parseProtocolV2Resources(
465
+ (data.neo as { resources?: unknown } | undefined)?.resources
466
+ );
458
467
  } catch (error) {
459
468
  // Firmware resource metadata is not required for base communication. If the
460
469
  // remote config is temporarily incomplete, disable this resource update only.
@@ -463,7 +472,9 @@ export default class DataManager {
463
472
  Log.warn('[DataConfig] Ignoring invalid Pro2 resources config:', error);
464
473
  }
465
474
  const enrichedPro2Config = this.enrichFirmwareReleaseInfo(data.pro2);
475
+ const enrichedNeoConfig = this.enrichFirmwareReleaseInfo(data.neo);
466
476
  const { resources: _unvalidatedResources, ...pro2Config } = enrichedPro2Config;
477
+ const { resources: _unvalidatedNeoResources, ...neoConfig } = enrichedNeoConfig;
467
478
  Log.log(`[DataConfig] Config loaded successfully via [${fetchMethod}]`);
468
479
  this.deviceMap = {
469
480
  [EDeviceType.Classic]: this.enrichFirmwareReleaseInfo(data.classic),
@@ -476,6 +487,10 @@ export default class DataManager {
476
487
  ...pro2Config,
477
488
  ...(pro2Resources ? { resources: pro2Resources } : undefined),
478
489
  },
490
+ [EDeviceType.Neo]: {
491
+ ...neoConfig,
492
+ ...(neoResources ? { resources: neoResources } : undefined),
493
+ },
479
494
  };
480
495
  this.assets = {
481
496
  bridge: data.bridge,
@@ -509,7 +524,9 @@ export default class DataManager {
509
524
  }
510
525
 
511
526
  /** Force a fresh remote config before an update is allowed to mutate the device. */
512
- static async forceReloadData(): Promise<void> {
527
+ static async forceReloadData({
528
+ requireResources = false,
529
+ }: { requireResources?: boolean } = {}): Promise<void> {
513
530
  if (!this.settings) {
514
531
  throw new Error('Remote config settings are not initialized');
515
532
  }
@@ -520,21 +537,23 @@ export default class DataManager {
520
537
  'Unable to refresh the latest remote config'
521
538
  );
522
539
  }
523
- if (this.protocolV2ResourcesConfigError) {
540
+ if (requireResources && this.protocolV2ResourcesConfigError) {
524
541
  throw ERRORS.TypedError(
525
- HardwareErrorCode.NetworkError,
542
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
526
543
  `Invalid Pro2 resources config: ${this.protocolV2ResourcesConfigError.message}`
527
544
  );
528
545
  }
529
546
  this.lastCheckTimestamp = getTimeStamp();
530
547
  }
531
548
 
532
- static getProtocolV2Resources() {
533
- return this.deviceMap[EDeviceType.Pro2]?.resources?.stable;
549
+ static getProtocolV2Resources(deviceType: EDeviceType.Pro2 | EDeviceType.Neo = EDeviceType.Pro2) {
550
+ return this.deviceMap[deviceType]?.resources?.stable;
534
551
  }
535
552
 
536
- static getProtocolV2BootResources() {
537
- return this.deviceMap[EDeviceType.Pro2]?.resources?.boot;
553
+ static getProtocolV2BootResources(
554
+ deviceType: EDeviceType.Pro2 | EDeviceType.Neo = EDeviceType.Pro2
555
+ ) {
556
+ return this.deviceMap[deviceType]?.resources?.boot;
538
557
  }
539
558
 
540
559
  static getProtobufMessages(schema: ProtobufMessageSchema = 'v1CurrentSchema'): JSON {
@@ -696,6 +696,7 @@ export class Device extends EventEmitter {
696
696
  // Most-specific model first; must match getMethodVersionRange in deviceInfoUtils,
697
697
  // otherwise e.g. Classic1s resolves the looser model_mini range before model_classic1s.
698
698
  const modelFallbacks: IDeviceModel[] = [
699
+ 'model_pro2',
699
700
  'model_classic1s',
700
701
  'model_classic',
701
702
  'model_mini',
@@ -715,7 +716,8 @@ export class Device extends EventEmitter {
715
716
  if (
716
717
  deviceType === EDeviceType.Touch ||
717
718
  deviceType === EDeviceType.Pro ||
718
- deviceType === EDeviceType.Pro2
719
+ deviceType === EDeviceType.Pro2 ||
720
+ deviceType === EDeviceType.Neo
719
721
  ) {
720
722
  return { support: true };
721
723
  }
@@ -732,7 +734,8 @@ export class Device extends EventEmitter {
732
734
  if (
733
735
  deviceType === EDeviceType.Touch ||
734
736
  deviceType === EDeviceType.Pro ||
735
- deviceType === EDeviceType.Pro2
737
+ deviceType === EDeviceType.Pro2 ||
738
+ deviceType === EDeviceType.Neo
736
739
  ) {
737
740
  return { support: false };
738
741
  }
@@ -1167,10 +1170,14 @@ export class Device extends EventEmitter {
1167
1170
  const protocolV2DeviceType = runtimeDeviceInfo
1168
1171
  ? resolveProtocolV2DeviceIdentity(runtimeDeviceInfo.hw?.Device_type).deviceType
1169
1172
  : this.getCurrentDeviceType();
1170
- if (runtimeMode === 'romloader' && protocolV2DeviceType !== EDeviceType.Pro2) {
1173
+ if (
1174
+ runtimeMode === 'romloader' &&
1175
+ protocolV2DeviceType !== EDeviceType.Pro2 &&
1176
+ protocolV2DeviceType !== EDeviceType.Neo
1177
+ ) {
1171
1178
  throw ERRORS.TypedError(
1172
1179
  HardwareErrorCode.DeviceInitializeFailed,
1173
- 'Protocol V2 romloader mode is only supported for Pro2.'
1180
+ 'Protocol V2 romloader mode is only supported for Pro2 and Neo.'
1174
1181
  );
1175
1182
  }
1176
1183
  const deviceStatusSupported = supportsProtocolV2Message(
@@ -1569,7 +1576,8 @@ export class Device extends EventEmitter {
1569
1576
  if (!this.state) return undefined;
1570
1577
  return (
1571
1578
  this.isProtocolV2() &&
1572
- this.getCurrentDeviceType() === EDeviceType.Pro2 &&
1579
+ (this.getCurrentDeviceType() === EDeviceType.Pro2 ||
1580
+ this.getCurrentDeviceType() === EDeviceType.Neo) &&
1573
1581
  this.state.status.mode === 'romloader'
1574
1582
  );
1575
1583
  }
@@ -1615,7 +1623,8 @@ export class Device extends EventEmitter {
1615
1623
  const isModeT =
1616
1624
  deviceType === EDeviceType.Touch ||
1617
1625
  deviceType === EDeviceType.Pro ||
1618
- deviceType === EDeviceType.Pro2;
1626
+ deviceType === EDeviceType.Pro2 ||
1627
+ deviceType === EDeviceType.Neo;
1619
1628
  const unlocked = this.state?.status.unlocked;
1620
1629
  const preCheckTouch = isModeT && unlocked === false;
1621
1630
  const passphraseProtection = this.getCurrentPassphraseProtection();
@@ -54,8 +54,16 @@ const DEVICE_SESSION_CALLS = new Set([
54
54
  'DeviceSessionAskPin',
55
55
  'DeviceSessionAskPassphrase',
56
56
  ]);
57
-
58
- const PROTOCOL_V2_ACTION_CANCELLED_SUBCODE = 1;
57
+ const DEVICE_CONTROL_CALLS = new Set([
58
+ 'DeviceReboot',
59
+ 'DeviceSettingsGet',
60
+ 'DeviceSettingsSet',
61
+ 'DeviceSettingsPageShow',
62
+ 'DeviceCertificateWrite',
63
+ 'DeviceCertificateRead',
64
+ 'DeviceCertificateSign',
65
+ 'DeviceMiscUsbMscControl',
66
+ ]);
59
67
 
60
68
  // Older Protocol V2 firmware may omit the cancellation subcode, so retain a
61
69
  // narrow message fallback for compatibility.
@@ -501,14 +509,9 @@ export class DeviceCommands {
501
509
  const normalizedMessage = message?.trim() ?? '';
502
510
  const isProtocolV2ActionCancelledFailure =
503
511
  this.device.isProtocolV2() && isProtocolV2ActionCancelledMessage(normalizedMessage);
504
- const isProtocolV2ActionCancelledSubcode =
505
- this.device.isProtocolV2() && subcode === PROTOCOL_V2_ACTION_CANCELLED_SUBCODE;
506
- // DeviceErrorCode currently reuses subcode 1 for Busy. Resolve that
507
- // domain-specific exception before applying the common cancellation map.
508
512
  const isProtocolV2DeviceBusyFailure =
509
513
  this.device.isProtocolV2() &&
510
- callType.startsWith('Device') &&
511
- !DEVICE_SESSION_CALLS.has(callType) &&
514
+ DEVICE_CONTROL_CALLS.has(callType) &&
512
515
  subcode === DeviceErrorCode.DeviceError_Busy;
513
516
  const isLegacyProtocolV2LockedFailure =
514
517
  this.device.isProtocolV2() && /^device (?:is )?locked$/i.test(normalizedMessage);
@@ -574,8 +577,8 @@ export class DeviceCommands {
574
577
  firmwareMessage: message,
575
578
  });
576
579
  } else if (
577
- subcode === DeviceErrorCode.DeviceError_ActionCancelled ||
578
- isProtocolV2ActionCancelledSubcode ||
580
+ (DEVICE_CONTROL_CALLS.has(callType) &&
581
+ subcode === DeviceErrorCode.DeviceError_ActionCancelled) ||
579
582
  isProtocolV2ActionCancelledFailure
580
583
  ) {
581
584
  error = ERRORS.TypedError(HardwareErrorCode.ActionCancelled, message, {
@@ -103,7 +103,7 @@ export class DevicePool extends EventEmitter {
103
103
  const device = this.devicesCache[connectId];
104
104
  if (device) {
105
105
  const exist = descriptorList.find(d => d.path === device.originalDescriptor.path);
106
- if (exist) {
106
+ if (exist && !initOptions?.forceProtocolDetection) {
107
107
  // Log.debug('find existed Device: ', connectId);
108
108
  device.updateDescriptor(exist, true);
109
109
  await this._refreshRuntimeState(device, initOptions);
@@ -151,14 +151,23 @@ export class DevicePool extends EventEmitter {
151
151
  }
152
152
  }
153
153
 
154
- static async _createDevice(descriptor: DeviceDescriptor, initOptions?: InitOptions) {
155
- let device = this.getDeviceByPath(descriptor.path);
156
- if (!device) {
157
- device = Device.fromDescriptor(descriptor);
154
+ static async _createDevice(
155
+ descriptor: DeviceDescriptor,
156
+ initOptions?: InitOptions
157
+ ): Promise<Device> {
158
+ const cachedDevice = this.getDeviceByPath(descriptor.path);
159
+ const forceProtocolDetection = initOptions?.forceProtocolDetection === true;
160
+ const isNewDevice = !cachedDevice;
161
+ const device = cachedDevice ?? Device.fromDescriptor(descriptor);
162
+ if (isNewDevice || forceProtocolDetection) {
158
163
  device.deviceConnector = this.connector;
159
- await device.connect(initOptions?.connectProtocol, {
160
- forceProtocolDetection: initOptions?.forceProtocolDetection,
161
- });
164
+ if (forceProtocolDetection && !isNewDevice) {
165
+ await device.acquire(initOptions?.connectProtocol, { forceProtocolDetection: true });
166
+ } else {
167
+ await device.connect(initOptions?.connectProtocol, {
168
+ forceProtocolDetection: initOptions?.forceProtocolDetection,
169
+ });
170
+ }
162
171
  try {
163
172
  await device.initialize(initOptions);
164
173
  if (initOptions?.refreshRuntimeState && device.isProtocolV2()) {
@@ -1,4 +1,4 @@
1
- import { EFirmwareType } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import {
4
4
  resolveDeviceBleFirmwareVersion,
@@ -386,7 +386,7 @@ export const buildProtocolV2FeaturesPayload = ({
386
386
  device_id: deviceId ?? undefined,
387
387
  session_id: cached?.sessionId ?? undefined,
388
388
  ble_name: bleName ?? undefined,
389
- onekey_device_type: 'PRO2',
389
+ onekey_device_type: deviceType === EDeviceType.Unknown ? undefined : deviceType.toUpperCase(),
390
390
  passphrase_protection: passphraseProtection ?? undefined,
391
391
  bootloader_mode: bootloaderMode,
392
392
  passphraseState: cached?.passphraseState,
@@ -26,6 +26,9 @@ export const resolveProtocolV2DeviceIdentity = (
26
26
  case DeviceType.PRO2:
27
27
  case 'PRO2':
28
28
  return { deviceType: EDeviceType.Pro2, model: 'pro2' };
29
+ case DeviceType.NEO:
30
+ case 'NEO':
31
+ return { deviceType: EDeviceType.Neo, model: 'neo' };
29
32
  default:
30
33
  return { deviceType: EDeviceType.Unknown, model: null };
31
34
  }
@@ -4,6 +4,7 @@ import { PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE } from '@onekeyfe/hd-transport';
4
4
  import type {
5
5
  IProtocolV2BootResources,
6
6
  IProtocolV2Resource,
7
+ IProtocolV2ResourceFile,
7
8
  IProtocolV2ResourceType,
8
9
  IProtocolV2Resources,
9
10
  } from '../../types';
@@ -247,23 +248,53 @@ function validateBootResources(value: unknown): IProtocolV2BootResources {
247
248
  if (resource.required !== false) {
248
249
  throw new Error('Invalid Pro2 boot resources required flag: expected false');
249
250
  }
250
- if (resource.target !== 'CRATE') {
251
- throw new Error('Invalid Pro2 boot resources target: expected CRATE');
251
+ if (resource.target !== 'RES') {
252
+ throw new Error('Invalid Pro2 boot resources target: expected RES');
252
253
  }
253
- if (typeof resource.url !== 'string' || !resource.url.startsWith('https://')) {
254
- throw new Error('Invalid Pro2 boot resources url');
254
+ if (
255
+ resource.manifestUrl !== undefined &&
256
+ (typeof resource.manifestUrl !== 'string' || !resource.manifestUrl.startsWith('https://'))
257
+ ) {
258
+ throw new Error('Invalid Pro2 boot resources manifestUrl');
255
259
  }
256
- if (!Number.isSafeInteger(resource.size) || Number(resource.size) <= 0) {
257
- throw new Error('Invalid Pro2 boot resources size');
260
+ if (!Array.isArray(resource.files) || resource.files.length === 0) {
261
+ throw new Error('Invalid Pro2 boot resources files');
262
+ }
263
+ const files = resource.files.map((value, index): IProtocolV2ResourceFile => {
264
+ if (!value || typeof value !== 'object') {
265
+ throw new Error(`Invalid Pro2 boot resource file at files[${index}]`);
266
+ }
267
+ const file = value as Partial<IProtocolV2ResourceFile>;
268
+ if (typeof file.url !== 'string' || !file.url.startsWith('https://')) {
269
+ throw new Error(`Invalid Pro2 boot resource url at files[${index}]`);
270
+ }
271
+ if (
272
+ typeof file.devicePath !== 'string' ||
273
+ !file.devicePath.startsWith('vol0:/') ||
274
+ file.devicePath.includes('..') ||
275
+ file.devicePath.includes('\\\\')
276
+ ) {
277
+ throw new Error(`Invalid Pro2 boot resource devicePath at files[${index}]`);
278
+ }
279
+ if (!Number.isSafeInteger(file.size) || Number(file.size) <= 0) {
280
+ throw new Error(`Invalid Pro2 boot resource size at files[${index}]`);
281
+ }
282
+ return {
283
+ ...(typeof file.name === 'string' && file.name ? { name: file.name } : undefined),
284
+ url: file.url,
285
+ devicePath: file.devicePath,
286
+ size: Number(file.size),
287
+ fileHash: normalizeHex(file.fileHash, SHA256_HEX_LENGTH, `boot files[${index}].fileHash`),
288
+ };
289
+ });
290
+ if (new Set(files.map(file => file.devicePath)).size !== files.length) {
291
+ throw new Error('Invalid Pro2 boot resources files: duplicate devicePath');
258
292
  }
259
293
  return {
260
294
  required: false,
261
- target: 'CRATE',
262
- url: resource.url,
263
- size: Number(resource.size),
264
- fileHash: normalizeHex(resource.fileHash, SHA256_HEX_LENGTH, 'boot fileHash'),
265
- payloadHash: normalizeHex(resource.payloadHash, SHA3_512_HEX_LENGTH, 'boot payloadHash'),
266
- headerHash: normalizeHex(resource.headerHash, SHA3_512_HEX_LENGTH, 'boot headerHash'),
295
+ target: 'RES',
296
+ ...(resource.manifestUrl ? { manifestUrl: resource.manifestUrl } : undefined),
297
+ files,
267
298
  };
268
299
  }
269
300
 
@@ -352,7 +383,7 @@ function bytesToHex(bytes: Uint8Array): string {
352
383
  /** Verify the complete downloaded file before any device mutation. */
353
384
  export function isProtocolV2ResourceFileValid(
354
385
  binary: ArrayBuffer,
355
- resource: Pick<IProtocolV2Resource, 'size' | 'fileHash'>
386
+ resource: Pick<IProtocolV2ResourceFile, 'size' | 'fileHash'>
356
387
  ): boolean {
357
388
  if (binary.byteLength !== resource.size) return false;
358
389
  return bytesToHex(sha256(new Uint8Array(binary))) === resource.fileHash.toLowerCase();
@@ -54,7 +54,7 @@ export type AllFirmwareRelease = {
54
54
  bridge?: FirmwareRelease;
55
55
  features?: Features;
56
56
  protocol?: 'V1' | 'V2';
57
- deviceType?: 'pro2';
57
+ deviceType?: 'pro2' | 'neo';
58
58
  firmwareType?: EFirmwareType;
59
59
  status?: ProtocolV2FirmwareReleaseStatus;
60
60
  hasUpgrade?: boolean;
@@ -83,8 +83,6 @@ export interface FirmwareUpdateV4Params {
83
83
  romloaderBinary?: ArrayBuffer;
84
84
  /** FW_MGMT_TARGET_BOOTLOADER = 3 */
85
85
  bootloaderBinary?: ArrayBuffer;
86
- /** FW_MGMT_TARGET_CRATE = 1; optional Pro2 startup resources. */
87
- bootResourcesBinary?: ArrayBuffer;
88
86
  /** FW_MGMT_TARGET_APPLICATION_P1 = 4 */
89
87
  applicationP1Binary?: ArrayBuffer;
90
88
  /** FW_MGMT_TARGET_APPLICATION_P2 = 5 */
@@ -98,12 +96,14 @@ export interface FirmwareUpdateV4Params {
98
96
  se04Binary?: ArrayBuffer;
99
97
  forcedUpdateRes?: boolean;
100
98
  /**
101
- * RESC bundle okpkg files written directly to devicePath through FilesystemFileWrite.
102
- * Manual mode installs them without version comparison.
99
+ * Arbitrary Protocol V2 resource files written directly with FilesystemFileWrite.
100
+ * Use this for manifest-driven boot resources and other non-RESC files.
103
101
  */
104
- resourceBundleFiles?: Array<{
102
+ resourceFiles?: Array<{
105
103
  binary: ArrayBuffer;
106
104
  devicePath: string;
105
+ size?: number;
106
+ fileHash?: string;
107
107
  }>;
108
108
  }
109
109
 
@@ -374,14 +374,21 @@ export type IDeviceType =
374
374
  | EDeviceType.Mini
375
375
  | EDeviceType.Touch
376
376
  | EDeviceType.Pro
377
- | EDeviceType.Pro2;
377
+ | EDeviceType.Pro2
378
+ | EDeviceType.Neo;
378
379
 
379
380
  /**
380
381
  * model_classic: 'classic' | 'classic1s' | 'classicpure'
381
382
  * model_mini: 'classic' | 'classic1s' | 'classicpure' | 'mini'
382
383
  * model_touch: 'touch' | 'pro'
384
+ * model_pro2: 'pro2' | 'neo'
383
385
  */
384
- export type IDeviceModel = 'model_classic' | 'model_mini' | 'model_touch' | 'model_classic1s';
386
+ export type IDeviceModel =
387
+ | 'model_classic'
388
+ | 'model_mini'
389
+ | 'model_touch'
390
+ | 'model_classic1s'
391
+ | 'model_pro2';
385
392
 
386
393
  export const DeviceModelToTypes: { [deviceModel in IDeviceModel]: IDeviceType[] } = {
387
394
  model_mini: [
@@ -391,6 +398,7 @@ export const DeviceModelToTypes: { [deviceModel in IDeviceModel]: IDeviceType[]
391
398
  EDeviceType.Mini,
392
399
  ],
393
400
  model_touch: [EDeviceType.Touch, EDeviceType.Pro],
401
+ model_pro2: [EDeviceType.Pro2, EDeviceType.Neo],
394
402
  model_classic: [EDeviceType.Classic, EDeviceType.Classic1s, EDeviceType.ClassicPure],
395
403
  model_classic1s: [EDeviceType.Classic1s, EDeviceType.ClassicPure],
396
404
  };
@@ -402,7 +410,8 @@ export const DeviceTypeToModels: { [deviceType in IDeviceType]: IDeviceModel[] }
402
410
  [EDeviceType.Mini]: ['model_mini'],
403
411
  [EDeviceType.Touch]: ['model_touch'],
404
412
  [EDeviceType.Pro]: ['model_touch'],
405
- [EDeviceType.Pro2]: [],
413
+ [EDeviceType.Pro2]: ['model_pro2'],
414
+ [EDeviceType.Neo]: ['model_pro2'],
406
415
  [EDeviceType.Unknown]: [],
407
416
  };
408
417
 
@@ -81,19 +81,22 @@ export type IProtocolV2Resource = {
81
81
  headerHash: string;
82
82
  };
83
83
 
84
- /** Optional Pro2 startup-resource CRATE installed by the bootloader updater. */
85
- export type IProtocolV2BootResources = {
86
- required: false;
87
- target: 'CRATE';
84
+ /** A file from the Protocol V2 resource manifest, written directly with FileWrite. */
85
+ export type IProtocolV2ResourceFile = {
86
+ name?: string;
88
87
  url: string;
89
- /** Complete CRATE file size in bytes. */
88
+ devicePath: string;
90
89
  size: number;
91
- /** SHA-256 of the complete CRATE file. */
90
+ /** SHA-256 of the complete file. */
92
91
  fileHash: string;
93
- /** SHA3-512 payload_hash from the signed CRATE header. */
94
- payloadHash: string;
95
- /** SHA3-512 header_hash from the signed CRATE header. */
96
- headerHash: string;
92
+ };
93
+
94
+ /** Optional Protocol V2 boot resources restored as individual files. */
95
+ export type IProtocolV2BootResources = {
96
+ required: false;
97
+ target: 'RES';
98
+ manifestUrl?: string;
99
+ files: IProtocolV2ResourceFile[];
97
100
  };
98
101
 
99
102
  export type IProtocolV2Resources = {
@@ -166,19 +169,25 @@ export type IBLEFirmwareReleaseInfo = {
166
169
  };
167
170
 
168
171
  type IKnownDevice = Exclude<IDeviceType, 'unknown'>;
172
+ type ILegacyKnownDevice = Exclude<IKnownDevice, 'neo'>;
173
+
174
+ type IDeviceReleaseInfo = {
175
+ firmware: IFirmwareReleaseInfo[];
176
+ /** Protocol V2 payload package set */
177
+ 'firmware-v1'?: IFirmwareReleaseInfo[];
178
+ 'firmware-v2'?: IFirmwareReleaseInfo[];
179
+ 'firmware-v8'?: IFirmwareReleaseInfo[];
180
+ 'firmware-btc-v8'?: IFirmwareReleaseInfo[];
181
+ ble: IBLEFirmwareReleaseInfo[];
182
+ /** Independent Protocol V2 resource release configuration. */
183
+ resources?: IProtocolV2Resources;
184
+ };
169
185
 
170
186
  export type DeviceTypeMap = {
171
- [k in IKnownDevice]: {
172
- firmware: IFirmwareReleaseInfo[];
173
- /** Pro2 Protocol V2 payload package set */
174
- 'firmware-v1'?: IFirmwareReleaseInfo[];
175
- 'firmware-v2'?: IFirmwareReleaseInfo[];
176
- 'firmware-v8'?: IFirmwareReleaseInfo[];
177
- 'firmware-btc-v8'?: IFirmwareReleaseInfo[];
178
- ble: IBLEFirmwareReleaseInfo[];
179
- /** Independent Pro2 resource release configuration. */
180
- resources?: IProtocolV2Resources;
181
- };
187
+ [k in ILegacyKnownDevice]: IDeviceReleaseInfo;
188
+ } & {
189
+ /** Optional until every remote-config producer publishes a Neo entry. */
190
+ neo?: IDeviceReleaseInfo;
182
191
  };
183
192
 
184
193
  export type AssetsMap = {
@@ -60,6 +60,8 @@ export const resolveDeviceType = (features?: DeviceFeaturesInput): IDeviceType =
60
60
  return EDeviceType.Pro;
61
61
  case 'PRO2':
62
62
  return EDeviceType.Pro2;
63
+ case 'NEO':
64
+ return EDeviceType.Neo;
63
65
  case 'PURE':
64
66
  return EDeviceType.ClassicPure;
65
67
  default:
@@ -69,6 +71,9 @@ export const resolveDeviceType = (features?: DeviceFeaturesInput): IDeviceType =
69
71
  if (compatible.model?.toString().toLowerCase() === EDeviceType.Pro2) {
70
72
  return EDeviceType.Pro2;
71
73
  }
74
+ if (compatible.model?.toString().toLowerCase() === EDeviceType.Neo) {
75
+ return EDeviceType.Neo;
76
+ }
72
77
 
73
78
  const serialNo = resolveDeviceSerialNo(features);
74
79
  const prefix = serialNo.slice(0, 2).toLowerCase();
@@ -74,7 +74,8 @@ export const supportInputPinOnSoftware = (features?: Features): SupportFeatureTy
74
74
  if (
75
75
  deviceType === EDeviceType.Touch ||
76
76
  deviceType === EDeviceType.Pro ||
77
- deviceType === EDeviceType.Pro2
77
+ deviceType === EDeviceType.Pro2 ||
78
+ deviceType === EDeviceType.Neo
78
79
  ) {
79
80
  return { support: false };
80
81
  }
@@ -90,7 +91,8 @@ export const supportNewPassphrase = (features?: Features): SupportFeatureType =>
90
91
  if (
91
92
  deviceType === EDeviceType.Touch ||
92
93
  deviceType === EDeviceType.Pro ||
93
- deviceType === EDeviceType.Pro2
94
+ deviceType === EDeviceType.Pro2 ||
95
+ deviceType === EDeviceType.Neo
94
96
  ) {
95
97
  return { support: true };
96
98
  }
@@ -143,7 +145,8 @@ export const getPassphraseStateWithRefreshDeviceInfo = async (
143
145
  const isModeT =
144
146
  deviceType === EDeviceType.Touch ||
145
147
  deviceType === EDeviceType.Pro ||
146
- deviceType === EDeviceType.Pro2;
148
+ deviceType === EDeviceType.Pro2 ||
149
+ deviceType === EDeviceType.Neo;
147
150
 
148
151
  // 如果可以获取到 passphraseState,但是设备 features 显示设备未开启 passphrase,需要刷新设备状态
149
152
  // if passphraseState can be obtained, but the device features show that the device has not enabled passphrase, the device status needs to be refreshed
@@ -31,6 +31,7 @@ export const getDeviceTypeByBleName = (name?: string): IDeviceType => {
31
31
  if (/^Touch/i.test(name)) return EDeviceType.Touch;
32
32
 
33
33
  if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name)) return EDeviceType.Pro2;
34
+ if (/\bNeo\b/i.test(name) || /^Neo/i.test(name)) return EDeviceType.Neo;
34
35
  if (/\bPro\b/i.test(name) || /^Pro/i.test(name)) return EDeviceType.Pro;
35
36
 
36
37
  return EDeviceType.Unknown;
@@ -93,6 +94,7 @@ export const getMethodVersionRange = (
93
94
  }
94
95
 
95
96
  const modelFallbacks: IDeviceModel[] = [
97
+ 'model_pro2',
96
98
  'model_classic1s',
97
99
  'model_classic',
98
100
  'model_mini',
@@ -127,6 +127,7 @@ export const getLanguageConfig = (deviceType: IDeviceType): Record<string, strin
127
127
  keys = Object.keys(LANGUAGE_LABELS) as LanguageKey[];
128
128
  break;
129
129
  case EDeviceType.Pro2:
130
+ case EDeviceType.Neo:
130
131
  return PRO2_LANGUAGE_OPTIONS.map(option => ({ ...option }));
131
132
  default:
132
133
  keys = [];
@@ -176,6 +177,7 @@ export const getAutoLockOptions = (
176
177
  case EDeviceType.Touch:
177
178
  case EDeviceType.Pro:
178
179
  case EDeviceType.Pro2:
180
+ case EDeviceType.Neo:
179
181
  return withNever([30_000, 60_000, 120_000, 300_000, 600_000, 1_800_000], protocol);
180
182
  default:
181
183
  return [];
@@ -197,6 +199,7 @@ export const getAutoShutDownOptions = (
197
199
  case EDeviceType.Pro:
198
200
  return withNever([60_000, 120_000, 300_000, 600_000], protocol);
199
201
  case EDeviceType.Pro2:
202
+ case EDeviceType.Neo:
200
203
  return withNever([60_000, 120_000, 300_000, 600_000, 1_800_000], protocol);
201
204
  default:
202
205
  return [];