11import { type BytesView } from "@zwave-js/shared" ;
2+ import { sha3_256 } from "@noble/hashes/sha3.js" ;
3+ import { bytesToHex } from "@noble/hashes/utils.js" ;
24
35/**
4- * Utility functions for downloading and verifying Z-Wave firmware from GitHub
6+ * Utility functions for downloading and verifying Z-Wave firmware
57 */
68
79export interface FirmwareDownloadResult {
@@ -26,84 +28,136 @@ export class FirmwareDownloadError extends Error {
2628
2729interface GitHubRelease {
2830 tag_name : string ;
29- assets : GitHubAsset [ ] ;
31+ draft : boolean ;
32+ prerelease : boolean ;
3033}
3134
32- interface GitHubAsset {
33- name : string ;
34- browser_download_url : string ;
35- digest ?: string ;
35+ interface FirmwareManifest {
36+ metadata : {
37+ created_at : string ;
38+ } ;
39+ firmwares : FirmwareManifestEntry [ ] ;
40+ }
41+
42+ interface FirmwareManifestEntry {
43+ filename : string ;
44+ checksum : string ;
45+ size : number ;
3646}
3747
48+ const SILABS_FIRMWARE_REPO = 'NabuCasa/silabs-firmware-builder' ;
49+ const RELEASES_BRANCH = 'releases' ;
50+ const RELEASES_LATEST_API_URL = `https://api.github.com/repos/${ SILABS_FIRMWARE_REPO } /releases/latest` ;
51+ const RELEASES_API_URL = `https://api.github.com/repos/${ SILABS_FIRMWARE_REPO } /releases?per_page=30` ;
52+ const RELEASES_BRANCH_RAW_BASE_URL =
53+ `https://raw.githubusercontent.com/${ SILABS_FIRMWARE_REPO } /${ RELEASES_BRANCH } ` ;
54+ const ZWA2_FIRMWARE_PREFIX = 'zwa2_controller' ;
55+
3856/**
39- * Downloads the latest Z-Wave firmware from the GitHub repository
57+ * Downloads the latest Z-Wave firmware from silabs-firmware-builder release manifests
4058 * @returns Promise resolving to firmware file name and data
4159 * @throws FirmwareDownloadError if download or verification fails
4260 */
4361export async function downloadLatestFirmware ( ) : Promise < FirmwareDownloadResult > {
4462 try {
45- // Fetch the latest release information
46- const releaseResponse = await fetch (
47- 'https://api.github.com/repos/NabuCasa/zwave-firmware/releases/latest'
48- ) ;
63+ let latestRelease : GitHubRelease | undefined ;
64+ const latestReleaseResponse = await fetch ( RELEASES_LATEST_API_URL ) ;
65+ if ( latestReleaseResponse . ok ) {
66+ latestRelease = await latestReleaseResponse . json ( ) ;
67+ }
68+
69+ const releaseResponse = await fetch ( RELEASES_API_URL ) ;
4970
50- if ( ! releaseResponse . ok ) {
71+ if ( ! releaseResponse . ok && ! latestRelease ) {
5172 throw new FirmwareDownloadError (
52- `Failed to fetch release information : ${ releaseResponse . status } ${ releaseResponse . statusText } `
73+ `Failed to fetch release list : ${ releaseResponse . status } ${ releaseResponse . statusText } `
5374 ) ;
5475 }
5576
56- const release : GitHubRelease = await releaseResponse . json ( ) ;
57-
58- // Find the GBL file in the assets
59- const gblAsset = release . assets . find ( asset =>
60- asset . name . toLowerCase ( ) . endsWith ( '.gbl' )
61- ) ;
77+ const releases : GitHubRelease [ ] = releaseResponse . ok
78+ ? await releaseResponse . json ( )
79+ : [ ] ;
80+ const stableReleases = releases . filter ( release => ! release . draft && ! release . prerelease ) ;
6281
63- if ( ! gblAsset ) {
64- throw new FirmwareDownloadError (
65- `No GBL firmware file found in release ${ release . tag_name } `
66- ) ;
82+ if (
83+ latestRelease &&
84+ ! stableReleases . some ( release => release . tag_name === latestRelease . tag_name )
85+ ) {
86+ stableReleases . unshift ( latestRelease ) ;
6787 }
6888
69- // Extract expected checksum from the digest property
70- let expectedChecksum : string | null = null ;
71- if ( gblAsset . digest ) {
72- // The digest format is "sha256:hash"
73- const digestMatch = gblAsset . digest . match ( / ^ s h a 2 5 6 : ( [ a - f A - F 0 - 9 ] { 64 } ) $ / ) ;
74- if ( digestMatch ) {
75- expectedChecksum = digestMatch [ 1 ] ;
76- }
89+ if ( stableReleases . length === 0 ) {
90+ throw new FirmwareDownloadError ( 'No stable releases found in firmware repository' ) ;
7791 }
7892
79- // Download the firmware file through a CORS proxy
80- // GitHub doesn't provide CORS headers for release downloads, so we need a proxy
81- const proxyUrl = `https://corsproxy.io/?${ encodeURIComponent ( gblAsset . browser_download_url ) } ` ;
82- const firmwareResponse = await fetch ( proxyUrl ) ;
93+ let lastReleaseError : unknown ;
8394
84- if ( ! firmwareResponse . ok ) {
85- throw new FirmwareDownloadError (
86- `Failed to download firmware: ${ firmwareResponse . status } ${ firmwareResponse . statusText } `
87- ) ;
88- }
95+ for ( const release of stableReleases ) {
96+ try {
97+ const manifestUrl = `${ RELEASES_BRANCH_RAW_BASE_URL } /${ release . tag_name } /manifest.json` ;
8998
90- const firmwareArrayBuffer = await firmwareResponse . arrayBuffer ( ) ;
91- const firmwareData = new Uint8Array ( firmwareArrayBuffer ) ;
99+ const manifestResponse = await fetch ( manifestUrl ) ;
100+ if ( ! manifestResponse . ok ) {
101+ if ( manifestResponse . status === 404 ) {
102+ continue ;
103+ }
92104
93- // Verify checksum if available
94- if ( expectedChecksum ) {
95- const actualChecksum = await calculateSHA256 ( firmwareData ) ;
96- if ( actualChecksum . toLowerCase ( ) !== expectedChecksum . toLowerCase ( ) ) {
97- throw new FirmwareDownloadError (
98- `Checksum verification failed. Expected: ${ expectedChecksum } , Got: ${ actualChecksum } `
105+ throw new FirmwareDownloadError (
106+ `Failed to fetch manifest for release ${ release . tag_name } : ${ manifestResponse . status } ${ manifestResponse . statusText } `
107+ ) ;
108+ }
109+
110+ const manifest : FirmwareManifest = await manifestResponse . json ( ) ;
111+ const firmwareEntry = manifest . firmwares . find (
112+ firmware =>
113+ firmware . filename . startsWith ( ZWA2_FIRMWARE_PREFIX ) &&
114+ firmware . filename . toLowerCase ( ) . endsWith ( '.gbl' )
99115 ) ;
116+
117+ if ( ! firmwareEntry ) {
118+ continue ;
119+ }
120+
121+ const firmwareUrl = `${ RELEASES_BRANCH_RAW_BASE_URL } /${ release . tag_name } /${ firmwareEntry . filename } ` ;
122+ const firmwareResponse = await fetch ( firmwareUrl ) ;
123+
124+ if ( ! firmwareResponse . ok ) {
125+ throw new FirmwareDownloadError (
126+ `Failed to download firmware from ${ release . tag_name } : ${ firmwareResponse . status } ${ firmwareResponse . statusText } `
127+ ) ;
128+ }
129+
130+ const firmwareArrayBuffer = await firmwareResponse . arrayBuffer ( ) ;
131+ const firmwareData = new Uint8Array ( firmwareArrayBuffer ) ;
132+
133+ if ( firmwareEntry . size !== firmwareData . length ) {
134+ throw new FirmwareDownloadError (
135+ `Firmware size verification failed for ${ firmwareEntry . filename } . Expected: ${ firmwareEntry . size } , Got: ${ firmwareData . length } `
136+ ) ;
137+ }
138+
139+ await verifyChecksum ( firmwareData , firmwareEntry . checksum ) ;
140+
141+ return {
142+ fileName : firmwareEntry . filename ,
143+ data : firmwareData ,
144+ } ;
145+ } catch ( error ) {
146+ lastReleaseError = error ;
147+ continue ;
100148 }
101149 }
102150
103- return {
104- fileName : gblAsset . name ,
105- data : firmwareData
106- } ;
151+ if ( lastReleaseError ) {
152+ throw new FirmwareDownloadError (
153+ `Unable to download ${ ZWA2_FIRMWARE_PREFIX } firmware from available releases` ,
154+ lastReleaseError
155+ ) ;
156+ }
157+
158+ throw new FirmwareDownloadError (
159+ `No ${ ZWA2_FIRMWARE_PREFIX } firmware found in available release manifests`
160+ ) ;
107161
108162 } catch ( error ) {
109163 if ( error instanceof FirmwareDownloadError ) {
@@ -137,3 +191,27 @@ async function calculateSHA256(data: Uint8Array): Promise<string> {
137191 const hashArray = Array . from ( new Uint8Array ( hashBuffer ) ) ;
138192 return hashArray . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
139193}
194+
195+ async function verifyChecksum ( data : Uint8Array , checksum : string ) : Promise < void > {
196+ const [ algorithm , expectedHash ] = checksum . split ( ':' , 2 ) ;
197+
198+ if ( ! algorithm || ! expectedHash ) {
199+ throw new FirmwareDownloadError ( `Invalid checksum format: ${ checksum } ` ) ;
200+ }
201+
202+ let actualHash : string ;
203+
204+ if ( algorithm . toLowerCase ( ) === 'sha256' ) {
205+ actualHash = await calculateSHA256 ( data ) ;
206+ } else if ( algorithm . toLowerCase ( ) === 'sha3-256' ) {
207+ actualHash = bytesToHex ( sha3_256 ( data ) ) ;
208+ } else {
209+ throw new FirmwareDownloadError ( `Unsupported checksum algorithm: ${ algorithm } ` ) ;
210+ }
211+
212+ if ( actualHash . toLowerCase ( ) !== expectedHash . toLowerCase ( ) ) {
213+ throw new FirmwareDownloadError (
214+ `Checksum verification failed. Expected: ${ expectedHash } , Got: ${ actualHash } `
215+ ) ;
216+ }
217+ }
0 commit comments