Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
81629c0
chore: release hardware SDK 1.2.0-alpha.66
wabicai Aug 6, 2026
af55c7d
feat: harden prepared firmware updates
originalix Jul 27, 2026
a3abd56
refactor: remove firmware recovery orchestration
originalix Jul 28, 2026
1601cd7
fix: address firmware update review findings
originalix Jul 28, 2026
14bd4fc
fix: address firmware sdk review findings
originalix Jul 28, 2026
d125343
fix: align forced firmware update plan
originalix Jul 29, 2026
5f5bf9e
fix: enforce firmware update invariants
originalix Jul 29, 2026
579c90d
fix: keep firmware reconnect failures retryable
originalix Jul 29, 2026
245adec
test: align firmware release protocol mock
originalix Jul 30, 2026
3af4956
fix: keep stale Initialize timeout from tearing down BLE transport
originalix Aug 4, 2026
3bf910f
fix: allow degraded-identity firmware plans for bootloader recovery
originalix Aug 4, 2026
8eb6af6
fix: bound BLE packet writes so a wedged link cannot hang the transport
originalix Aug 4, 2026
20a61dd
fix: bound BLE connect so a stalled native connect cannot block a minute
originalix Aug 4, 2026
0a8c72d
chore: bump hardware sdk prerelease
originalix Aug 4, 2026
d057f7e
fix: let a bootloader recovery plan follow the device that reports it…
originalix Aug 5, 2026
9026ed7
chore: bump hardware sdk prerelease
originalix Aug 5, 2026
20eeaf0
fix: stop re-probing the other protocol while a known device reboots
originalix Aug 5, 2026
2ac875b
chore: bump hardware sdk prerelease
originalix Aug 5, 2026
da48cf8
fix: bound BLE GATT setup after device reboot
originalix Aug 5, 2026
b7addcd
chore: bump hardware sdk prerelease
originalix Aug 5, 2026
994b46c
fix: preserve firmware update errors
originalix Aug 6, 2026
4f22fb7
refactor: remove unused firmware resource artifact
originalix Aug 6, 2026
9df1842
chore: bump hardware sdk prerelease
originalix Aug 6, 2026
a344201
docs: clarify firmware artifact integrity boundary
originalix Aug 6, 2026
9aadbcc
fix: resolve firmware upgrade rebase conflicts
originalix Aug 6, 2026
de2bfce
chore: bump hardware sdk prerelease
originalix Aug 6, 2026
6e8a931
chore: refresh hardware sdk lockfile
originalix Aug 6, 2026
551e27f
chore: remove transient publish lock entries
originalix Aug 6, 2026
c6baed2
chore: release hardware SDK 1.2.0-alpha.69
wabicai Aug 6, 2026
227960b
fix: keep sdk available without firmware manifest
originalix Aug 6, 2026
8da6f44
chore: bump hardware sdk prerelease
originalix Aug 6, 2026
f41c733
chore: bump hardware sdk prerelease to alpha.70
originalix Aug 6, 2026
d86b033
chore: update sdk release lockfile
originalix Aug 6, 2026
c61db52
chore: merge sdk updates and bump alpha.71
originalix Aug 7, 2026
c58f39f
chore: refresh sdk release lockfile
originalix Aug 7, 2026
951dec8
chore: merge onekey after pr855 and resolve firmware conflicts
originalix Aug 7, 2026
76311cc
chore: bump hardware sdk prerelease to alpha.72
originalix Aug 7, 2026
93199b8
fix: keep iOS bulk BLE writes on writeWithoutResponse
originalix Aug 7, 2026
5fd2c61
chore: bump hardware sdk prerelease to alpha.73
originalix Aug 7, 2026
b03e1fa
fix: restore lost SESSION_ERROR constant in Protocol V2 file write
originalix Aug 7, 2026
1332017
style: sort firmware update imports
originalix Aug 7, 2026
4b263fb
chore: bump hardware sdk prerelease to alpha.74
originalix Aug 7, 2026
9c85b37
Merge remote-tracking branch 'origin/onekey' into codex/firmware-upgr…
originalix Aug 7, 2026
f3f7a26
fix: harden legacy firmware recovery
originalix Aug 7, 2026
2d53efc
chore: bump hardware sdk prerelease to alpha.77
originalix Aug 7, 2026
f205d4b
Merge branch 'onekey' into codex/firmware-upgrade-sdk-minimal
wabicai Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/architecture/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ Pro2 acquire 后的初始化、重连和固件升级重连统一读取 `Protocol
- `packages/core/src/device/Device.ts`
- `packages/core/src/api/FirmwareUpdateV4.ts`

## Prepared 固件 Artifact 完整性边界

Prepared 固件更新将 artifact 获取与设备执行分离,完整性责任按以下边界划分:

- 外部固件 Host(例如 App 的 native/desktop artifact store)负责从可信发布元数据取得预期大小和
SHA-256,对实际下载字节完成校验,并在首次设备变异前生成 receipt。
- `artifactRef` 必须引用已经校验的内容寻址对象;Host 必须在 lease 和 reader 生命周期内保持对象
不可变,并在对象缺失或损坏时让 `open` 失败。
- SDK 负责校验 Plan、PreparedPlan 和 receipt 的元数据绑定、artifact 大小、读取范围与 EOF,
但不在执行期重新计算 artifact 内容的 SHA-256。`FirmwareArtifactReceiptMismatch` 表示绑定或
reader 契约不匹配,不表示 SDK 已独立认证实际字节内容。
- “首次设备变异前已完成完整性校验”的保证依赖外部 Host 履行上述契约。设备端固件签名校验是
独立防线,不能替代 Host 对资源 artifact 的完整性校验。

主要实现:

- `packages/core/src/api/firmware/FirmwareUpdatePreparedPlan.ts`
- `packages/core/src/api/firmware/FirmwareArtifactSource.ts`

## 维护规则

- 只有持续影响多个模块、不能仅从代码局部理解的规则才进入本文。
Expand Down
4 changes: 2 additions & 2 deletions packages/connect-examples/electron-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hardware-example",
"productName": "HardwareExample",
"executableName": "onekey-hardware-example",
"version": "1.2.0-alpha.75",
"version": "1.2.0-alpha.76",
"author": "OneKey",
"description": "OneKey Hardware SDK Electron BLE example",
"main": "dist/index.js",
Expand All @@ -21,7 +21,7 @@
"ts:check": "yarn tsc --noEmit"
},
"dependencies": {
"@onekeyfe/hd-transport-electron": "1.2.0-alpha.75",
"@onekeyfe/hd-transport-electron": "1.2.0-alpha.76",
"@stoprocent/noble": "2.3.16",
"debug": "4.3.4",
"electron-is-dev": "^3.0.1",
Expand Down
12 changes: 6 additions & 6 deletions packages/connect-examples/expo-example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "expo-example",
"version": "1.2.0-alpha.75",
"version": "1.2.0-alpha.76",
"scripts": {
"start": "yarn expo start --dev-client",
"android": "yarn expo run:android",
Expand All @@ -19,11 +19,11 @@
"@noble/ed25519": "^2.1.0",
"@noble/hashes": "^1.3.3",
"@noble/secp256k1": "^1.7.1",
"@onekeyfe/hd-ble-sdk": "1.2.0-alpha.75",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.75",
"@onekeyfe/hd-core": "1.2.0-alpha.75",
"@onekeyfe/hd-shared": "1.2.0-alpha.75",
"@onekeyfe/hd-transport": "1.2.0-alpha.75",
"@onekeyfe/hd-ble-sdk": "1.2.0-alpha.76",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.76",
"@onekeyfe/hd-core": "1.2.0-alpha.76",
"@onekeyfe/hd-shared": "1.2.0-alpha.76",
"@onekeyfe/hd-transport": "1.2.0-alpha.76",
"@onekeyfe/react-native-ble-utils": "^0.1.3",
"@polkadot/util-crypto": "13.1.1",
"@react-native-async-storage/async-storage": "1.21.0",
Expand Down
10 changes: 5 additions & 5 deletions packages/connect-examples/expo-playground/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "onekey-hardware-playground",
"version": "1.2.0-alpha.75",
"version": "1.2.0-alpha.76",
"private": true,
"sideEffects": [
"app/utils/shim.js",
Expand All @@ -18,10 +18,10 @@
},
"dependencies": {
"@noble/hashes": "^1.8.0",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.75",
"@onekeyfe/hd-core": "1.2.0-alpha.75",
"@onekeyfe/hd-shared": "1.2.0-alpha.75",
"@onekeyfe/hd-transport": "1.2.0-alpha.75",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.76",
"@onekeyfe/hd-core": "1.2.0-alpha.76",
"@onekeyfe/hd-shared": "1.2.0-alpha.76",
"@onekeyfe/hd-transport": "1.2.0-alpha.76",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-examples/hwk-demo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hwk-demo",
"version": "1.2.0-alpha.75",
"version": "1.2.0-alpha.76",
"private": true,
"main": "index.js",
"scripts": {
Expand Down
50 changes: 45 additions & 5 deletions packages/core/__tests__/connectSettings.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
import DataManager from '../src/data-manager/DataManager';
import { parseConnectSettings } from '../src/data-manager/connectSettings';

import type { RemoteConfigResponse } from '../src/types';

jest.mock('axios', () => ({
get: jest.fn(),
}));

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

const mockAxiosGet: jest.Mock = jest.requireMock('axios').get;

describe('parseConnectSettings', () => {
it('keeps custom configFetcher in parsed settings', () => {
const configFetcher = jest.fn();
const preloadedConfig = {
bridge: {},
classic: { firmware: [], ble: [] },
classic1s: { firmware: [], ble: [] },
classicpure: { firmware: [], ble: [] },
mini: { firmware: [], ble: [] },
touch: { firmware: [], ble: [] },
pro: { firmware: [], ble: [] },
pro2: { firmware: [], ble: [] },
} as unknown as RemoteConfigResponse;

it('keeps a serializable preloaded config in parsed settings', () => {
const settings = parseConnectSettings({
firmwareManifestMode: 'sdk-managed',
preloadedConfig,
});

expect(settings.firmwareManifestMode).toBe('sdk-managed');
expect(settings.preloadedConfig).toBe(preloadedConfig);
});

it('uses the preloaded config before the SDK network client', async () => {
const settings = parseConnectSettings({
firmwareManifestMode: 'sdk-managed',
preloadedConfig,
});

await DataManager.load(settings);

expect(mockAxiosGet).not.toHaveBeenCalled();
expect(DataManager.deviceMap.classic).toEqual({ firmware: [], ble: [] });
});

it('keeps the SDK offline and available without an external manifest snapshot', async () => {
const settings = parseConnectSettings({
fetchConfig: true,
configFetcher,
firmwareManifestMode: 'external-only',
});

expect(settings.configFetcher).toBe(configFetcher);
await expect(DataManager.load(settings)).resolves.toBe(false);
expect(mockAxiosGet).not.toHaveBeenCalled();
});
});
56 changes: 56 additions & 0 deletions packages/core/__tests__/device-initialize-timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Device } from '../src/device/Device';
import TransportManager from '../src/data-manager/TransportManager';

jest.mock('../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

function buildV1Device() {
const device = Object.create(Device.prototype) as Device;
const typedCall = jest.fn(() => Promise.resolve({ message: { device_id: 'device' } }));
Object.assign(device, {
commands: { typedCall },
stateStore: { getState: () => undefined },
isProtocolV2: () => false,
_updateFeatures: jest.fn(),
getInternalState: jest.fn(() => undefined),
});
return { device, typedCall };
}

describe('Device.initialize Protocol V1 timeout', () => {
beforeEach(() => {
jest.spyOn(TransportManager, 'reconfigure').mockResolvedValue(undefined);
});

afterEach(() => {
jest.restoreAllMocks();
});

test('passes a custom Initialize timeout through to typedCall', async () => {
const { device, typedCall } = buildV1Device();

await device.initialize({ timeoutMs: 2500 });

expect(typedCall).toHaveBeenCalledWith(
'Initialize',
'Features',
expect.objectContaining({ is_contains_attach: true }),
{ timeoutMs: 2500 }
);
});

test('keeps the 25s Initialize timeout by default', async () => {
const { device, typedCall } = buildV1Device();

await device.initialize();

expect(typedCall).toHaveBeenCalledWith(
'Initialize',
'Features',
expect.objectContaining({ is_contains_attach: true }),
{ timeoutMs: 25 * 1000 }
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';

import CheckAllFirmwareRelease from '../../src/api/CheckAllFirmwareRelease';
import { buildFirmwareUpdatePlan } from '../../src/api/firmware/FirmwareUpdatePlan';
import {
getBleFirmwareReleaseInfo,
getBootloaderReleaseInfo,
getFirmwareReleaseInfo,
} from '../../src/api/firmware/releaseHelper';

jest.mock('../../src/data/config', () => ({
getSDKVersion: jest.fn(() => '1.0.0'),
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
}));

jest.mock('../../src/api/firmware/FirmwareUpdatePlan', () => ({
buildFirmwareUpdatePlan: jest.fn(),
validateFirmwareUpdatePlanForceTargets: jest.fn(value => value ?? []),
}));

jest.mock('../../src/api/firmware/releaseHelper', () => ({
getBleFirmwareReleaseInfo: jest.fn(),
getBootloaderReleaseInfo: jest.fn(),
getFirmwareReleaseInfo: jest.fn(),
}));

const mockBuildFirmwareUpdatePlan = buildFirmwareUpdatePlan as jest.MockedFunction<
typeof buildFirmwareUpdatePlan
>;
const mockGetBleFirmwareReleaseInfo = getBleFirmwareReleaseInfo as jest.MockedFunction<
typeof getBleFirmwareReleaseInfo
>;
const mockGetBootloaderReleaseInfo = getBootloaderReleaseInfo as jest.MockedFunction<
typeof getBootloaderReleaseInfo
>;
const mockGetFirmwareReleaseInfo = getFirmwareReleaseInfo as jest.MockedFunction<
typeof getFirmwareReleaseInfo
>;

const noUpdate = {
status: 'valid' as const,
release: undefined,
};

const createMethod = (serialNo = 'device-id') => {
const method = new CheckAllFirmwareRelease({
id: 1,
payload: {
method: 'checkAllFirmwareRelease',
platform: 'native',
},
});
method.init();
method.device = {
isProtocolV2: jest.fn(() => false),
features: {
deviceType: EDeviceType.Classic1s,
serialNo,
firmwareType: EFirmwareType.Universal,
firmwareVersion: '1.0.0',
bootloaderVersion: '1.0.0',
},
} as typeof method.device;
return method;
};

const createMethodWithForceTargets = () => {
const method = createMethod();
method.payload = {
...method.payload,
forceUpdateTargets: ['firmware', 'resource'],
};
return method;
};

describe('CheckAllFirmwareRelease', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetFirmwareReleaseInfo.mockReturnValue(noUpdate);
mockGetBootloaderReleaseInfo.mockReturnValue(noUpdate);
mockGetBleFirmwareReleaseInfo.mockReturnValue(noUpdate);
});

test('keeps release information available when an optional Plan cannot be built', async () => {
const firmware = {
status: 'outdated' as const,
release: {
url: 'https://firmware.onekey.so/classic/firmware.bin',
version: [3, 0, 0],
},
};
mockGetFirmwareReleaseInfo.mockReturnValue(firmware);
mockBuildFirmwareUpdatePlan.mockImplementation(() => {
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Plan cannot be built', {
firmwareUpdateCode: 'FirmwarePlanInvalid',
});
});

await expect(createMethod('').run()).resolves.toEqual(
expect.objectContaining({
firmware,
bootloader: noUpdate,
ble: noUpdate,
firmwareUpdatePlan: undefined,
})
);
});

test('returns an optional Plan when the builder succeeds', async () => {
const plan = {
schemaVersion: 2 as const,
planDigest: 'a'.repeat(64),
executor: 'v3' as const,
deviceIdentity: 'device-id',
deviceModel: EDeviceType.Classic1s,
firmwareType: EFirmwareType.Universal,
platform: 'native' as const,
artifacts: [],
targetsToUpdate: [],
};
mockBuildFirmwareUpdatePlan.mockReturnValue(plan);

await expect(createMethod().run()).resolves.toEqual(
expect.objectContaining({
firmwareUpdatePlan: plan,
})
);
});

test('passes host-selected update targets to the Plan builder', async () => {
mockBuildFirmwareUpdatePlan.mockReturnValue({
schemaVersion: 2,
planDigest: 'a'.repeat(64),
executor: 'v2',
deviceIdentity: 'device-id',
deviceModel: EDeviceType.Classic1s,
firmwareType: EFirmwareType.Universal,
platform: 'native',
artifacts: [],
targetsToUpdate: [],
});

await createMethodWithForceTargets().run();

expect(mockBuildFirmwareUpdatePlan).toHaveBeenCalledWith(
expect.objectContaining({
forceUpdateTargets: ['firmware', 'resource'],
})
);
});

test('does not hide a Plan failure for an explicitly forced target', async () => {
const planError = ERRORS.TypedError(
HardwareErrorCode.RuntimeError,
'Forced firmware target is unavailable',
{
firmwareUpdateCode: 'FirmwarePlanInvalid',
}
);
mockBuildFirmwareUpdatePlan.mockImplementation(() => {
throw planError;
});

await expect(createMethodWithForceTargets().run()).rejects.toBe(planError);
});

test('does not hide unexpected Plan builder failures', async () => {
const unexpectedError = new Error('unexpected builder failure');
mockBuildFirmwareUpdatePlan.mockImplementation(() => {
throw unexpectedError;
});

await expect(createMethod().run()).rejects.toBe(unexpectedError);
});
});
Loading
Loading