homey-lib 2.45.4 → 2.46.1
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/assets/app/schema.d.ts +39 -0
- package/assets/app/schema.json +136 -0
- package/helpers/index.js +10 -0
- package/index.js +1 -0
- package/lib/App/index.js +96 -15
- package/lib/Util/file.js +84 -0
- package/lib/Util/index.js +62 -0
- package/lib/Util/zigbee.js +225 -0
- package/package.json +1 -1
- package/types/assets/app/schema.d.ts +39 -0
- package/types/helpers/index.d.ts +1 -0
- package/types/helpers/index.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/lib/App/index.d.ts.map +1 -1
- package/types/lib/Util/file.d.ts +27 -0
- package/types/lib/Util/file.d.ts.map +1 -0
- package/types/lib/Util/index.d.ts +53 -0
- package/types/lib/Util/index.d.ts.map +1 -1
- package/types/lib/Util/zigbee.d.ts +88 -0
- package/types/lib/Util/zigbee.d.ts.map +1 -0
- package/webpack/index.js +1 -1
- package/webpack.config.js +1 -0
package/assets/app/schema.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export type DriverSettings = (
|
|
|
80
80
|
[k: string]: unknown;
|
|
81
81
|
}
|
|
82
82
|
)[];
|
|
83
|
+
export type Integrity = string;
|
|
83
84
|
export type AppSettings = (
|
|
84
85
|
| {
|
|
85
86
|
type: "text" | "password" | "textarea" | "label";
|
|
@@ -247,6 +248,7 @@ export interface App {
|
|
|
247
248
|
[k: string]: unknown;
|
|
248
249
|
}[];
|
|
249
250
|
settings?: DriverSettings;
|
|
251
|
+
firmwareUpdates?: ZigbeeFirmwareUpdates;
|
|
250
252
|
gtin?: string | string[];
|
|
251
253
|
matter?: MatterDevice;
|
|
252
254
|
zwave?: ZwaveDevice;
|
|
@@ -373,6 +375,43 @@ export interface ZwaveSetting {
|
|
|
373
375
|
signed?: boolean;
|
|
374
376
|
[k: string]: unknown;
|
|
375
377
|
}
|
|
378
|
+
export interface ZigbeeFirmwareUpdates {
|
|
379
|
+
queryNextImageTimeout?: number;
|
|
380
|
+
minImageBlockPeriod?: number;
|
|
381
|
+
maxImageBlockSize?: number;
|
|
382
|
+
imageBlockRequestTimeout?: number;
|
|
383
|
+
upgradeEndRequestTimeout?: number;
|
|
384
|
+
upgradeEndDelay?: number;
|
|
385
|
+
postUpgradeAnnounceTimeout?: number;
|
|
386
|
+
/**
|
|
387
|
+
* @minItems 1
|
|
388
|
+
*/
|
|
389
|
+
updates: [ZigbeeFirmwareUpdate, ...ZigbeeFirmwareUpdate[]];
|
|
390
|
+
}
|
|
391
|
+
export interface ZigbeeFirmwareUpdate {
|
|
392
|
+
changelog: I18NObject;
|
|
393
|
+
device: ZigbeeFirmwareUpdateDevice;
|
|
394
|
+
/**
|
|
395
|
+
* @minItems 1
|
|
396
|
+
*/
|
|
397
|
+
files: [ZigbeeFirmwareUpdateFile, ...ZigbeeFirmwareUpdateFile[]];
|
|
398
|
+
}
|
|
399
|
+
export interface ZigbeeFirmwareUpdateDevice {
|
|
400
|
+
manufacturerName: string | string[];
|
|
401
|
+
productId: string | string[];
|
|
402
|
+
}
|
|
403
|
+
export interface ZigbeeFirmwareUpdateFile {
|
|
404
|
+
fileVersion: number;
|
|
405
|
+
imageType: number;
|
|
406
|
+
manufacturerCode: number;
|
|
407
|
+
minFileVersion?: number;
|
|
408
|
+
maxFileVersion?: number;
|
|
409
|
+
minHardwareVersion?: number;
|
|
410
|
+
maxHardwareVersion?: number;
|
|
411
|
+
size: number;
|
|
412
|
+
name: string;
|
|
413
|
+
integrity: Integrity;
|
|
414
|
+
}
|
|
376
415
|
export interface MatterDevice {
|
|
377
416
|
vendorId: number | number[];
|
|
378
417
|
productId: number | number[];
|
package/assets/app/schema.json
CHANGED
|
@@ -32,6 +32,135 @@
|
|
|
32
32
|
},
|
|
33
33
|
"additionalProperties": false
|
|
34
34
|
},
|
|
35
|
+
"integrity": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^(blake2b512|blake2s256|sha256|sha384|sha512|sha512-256|sha3-256|sha3-384|sha3-512):[0-9a-fA-F]+$"
|
|
38
|
+
},
|
|
39
|
+
"zigbee-firmware-update-device": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["manufacturerName", "productId"],
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"manufacturerName": {
|
|
45
|
+
"oneOf": [
|
|
46
|
+
{
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "string"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"productId": {
|
|
58
|
+
"oneOf": [
|
|
59
|
+
{
|
|
60
|
+
"type": "string"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "string"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"zigbee-firmware-update-file": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": ["fileVersion", "size", "imageType", "manufacturerCode", "name", "integrity"],
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"properties": {
|
|
77
|
+
"fileVersion": {
|
|
78
|
+
"type": "number"
|
|
79
|
+
},
|
|
80
|
+
"imageType": {
|
|
81
|
+
"type": "number"
|
|
82
|
+
},
|
|
83
|
+
"manufacturerCode": {
|
|
84
|
+
"type": "number"
|
|
85
|
+
},
|
|
86
|
+
"minFileVersion": {
|
|
87
|
+
"type": "number"
|
|
88
|
+
},
|
|
89
|
+
"maxFileVersion": {
|
|
90
|
+
"type": "number"
|
|
91
|
+
},
|
|
92
|
+
"minHardwareVersion": {
|
|
93
|
+
"type": "number"
|
|
94
|
+
},
|
|
95
|
+
"maxHardwareVersion": {
|
|
96
|
+
"type": "number"
|
|
97
|
+
},
|
|
98
|
+
"size": {
|
|
99
|
+
"type": "number"
|
|
100
|
+
},
|
|
101
|
+
"name": {
|
|
102
|
+
"type": "string"
|
|
103
|
+
},
|
|
104
|
+
"integrity": {
|
|
105
|
+
"$ref": "#/definitions/integrity"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"zigbee-firmware-update": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"required": ["changelog", "device", "files"],
|
|
112
|
+
"additionalProperties": false,
|
|
113
|
+
"properties": {
|
|
114
|
+
"changelog": {
|
|
115
|
+
"$ref": "#/definitions/i18nObject"
|
|
116
|
+
},
|
|
117
|
+
"device": {
|
|
118
|
+
"$ref": "#/definitions/zigbee-firmware-update-device"
|
|
119
|
+
},
|
|
120
|
+
"files": {
|
|
121
|
+
"type": "array",
|
|
122
|
+
"minItems": 1,
|
|
123
|
+
"items": {
|
|
124
|
+
"$ref": "#/definitions/zigbee-firmware-update-file"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"zigbee-firmware-updates": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"additionalProperties": false,
|
|
132
|
+
"required": ["updates"],
|
|
133
|
+
"properties": {
|
|
134
|
+
"queryNextImageTimeout": {
|
|
135
|
+
"type": "number"
|
|
136
|
+
},
|
|
137
|
+
"minImageBlockPeriod": {
|
|
138
|
+
"type": "number"
|
|
139
|
+
},
|
|
140
|
+
"maxImageBlockSize": {
|
|
141
|
+
"type": "number"
|
|
142
|
+
},
|
|
143
|
+
"imageBlockRequestTimeout": {
|
|
144
|
+
"type": "number"
|
|
145
|
+
},
|
|
146
|
+
"upgradeEndRequestTimeout": {
|
|
147
|
+
"type": "number"
|
|
148
|
+
},
|
|
149
|
+
"upgradeEndDelay": {
|
|
150
|
+
"type": "number"
|
|
151
|
+
},
|
|
152
|
+
"postUpgradeAnnounceTimeout": {
|
|
153
|
+
"type": "number"
|
|
154
|
+
},
|
|
155
|
+
"updates": {
|
|
156
|
+
"type": "array",
|
|
157
|
+
"minItems": 1,
|
|
158
|
+
"items": {
|
|
159
|
+
"$ref": "#/definitions/zigbee-firmware-update"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
35
164
|
"author": {
|
|
36
165
|
"required": ["name"],
|
|
37
166
|
"properties": {
|
|
@@ -1316,6 +1445,13 @@
|
|
|
1316
1445
|
"settings": {
|
|
1317
1446
|
"$ref": "#/definitions/driverSettings"
|
|
1318
1447
|
},
|
|
1448
|
+
"firmwareUpdates": {
|
|
1449
|
+
"oneOf": [
|
|
1450
|
+
{
|
|
1451
|
+
"$ref": "#/definitions/zigbee-firmware-updates"
|
|
1452
|
+
}
|
|
1453
|
+
]
|
|
1454
|
+
},
|
|
1319
1455
|
"gtin": {
|
|
1320
1456
|
"oneOf": [
|
|
1321
1457
|
{
|
package/helpers/index.js
CHANGED
package/index.js
CHANGED
|
@@ -47,3 +47,4 @@ module.exports.getBatteries = Energy.getBatteries.bind(Energy);
|
|
|
47
47
|
|
|
48
48
|
/** @typedef {import('./assets/app/schema').App} AppManifest */
|
|
49
49
|
/** @typedef {import('./assets/capability/schema').Capability} CapabilityDefinition */
|
|
50
|
+
/** @typedef {import('./assets/app/schema').ZigbeeFirmwareUpdates} ZigbeeFirmwareUpdates */
|
package/lib/App/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const Device = require('../Device');
|
|
|
14
14
|
const Capability = require('../Capability');
|
|
15
15
|
const Signal = require('../Signal');
|
|
16
16
|
const Energy = require('../Energy');
|
|
17
|
+
const Util = require('../Util');
|
|
17
18
|
|
|
18
19
|
const {
|
|
19
20
|
openAsync,
|
|
@@ -28,6 +29,7 @@ const {
|
|
|
28
29
|
extname,
|
|
29
30
|
basename,
|
|
30
31
|
dirname,
|
|
32
|
+
toArray,
|
|
31
33
|
} = require('../../helpers');
|
|
32
34
|
|
|
33
35
|
/** @typedef {import('../../assets/app/schema').App} AppManifest */
|
|
@@ -384,32 +386,29 @@ class App {
|
|
|
384
386
|
// validate `appJson.drivers[].capabilitiesOptions`
|
|
385
387
|
if (driver.capabilitiesOptions) {
|
|
386
388
|
for (const [capabilityId, capabilityOptions] of Object.entries(driver.capabilitiesOptions)) {
|
|
387
|
-
//
|
|
388
|
-
if (Capability.isInstanceOfId(capabilityId, 'target_power_mode')) {
|
|
389
|
+
// Validate target_power_mode values - check canonical IDs present, no reserved prefixes
|
|
390
|
+
if (Capability.isInstanceOfId(capabilityId, 'target_power_mode') && Array.isArray(capabilityOptions.values)) {
|
|
389
391
|
const canonicalValues = Capability.getCapability('target_power_mode').values;
|
|
390
|
-
const providedValues = Array.isArray(capabilityOptions.values) ? capabilityOptions.values : [];
|
|
391
392
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (seenIds.has(value.id)) continue;
|
|
398
|
-
seenIds.add(value.id);
|
|
399
|
-
extraValues.push(value);
|
|
393
|
+
for (const canonical of canonicalValues) {
|
|
394
|
+
const provided = capabilityOptions.values.find(v => v && typeof v === 'object' && v.id === canonical.id);
|
|
395
|
+
if (!provided) {
|
|
396
|
+
throw new Error(`drivers.${driver.id}.capabilitiesOptions.${capabilityId}.values must include canonical value "${canonical.id}"`);
|
|
397
|
+
}
|
|
400
398
|
}
|
|
401
399
|
|
|
402
|
-
const
|
|
400
|
+
const canonicalIds = new Set(canonicalValues.map(v => v.id));
|
|
401
|
+
const extraValues = capabilityOptions.values.filter(v => v && typeof v === 'object' && typeof v.id === 'string' && !canonicalIds.has(v.id));
|
|
402
|
+
|
|
403
|
+
const RESERVED_PREFIXES = ['homey_'];
|
|
403
404
|
const reservedValues = extraValues.filter(value =>
|
|
404
405
|
RESERVED_PREFIXES.some(prefix => value.id.startsWith(prefix))
|
|
405
406
|
);
|
|
406
407
|
|
|
407
408
|
if (reservedValues.length) {
|
|
408
409
|
const ids = reservedValues.map(value => value.id).join(', ');
|
|
409
|
-
throw new Error(`drivers.${driver.id}.capabilitiesOptions.${capabilityId} custom values cannot use reserved
|
|
410
|
+
throw new Error(`drivers.${driver.id}.capabilitiesOptions.${capabilityId}.values custom values cannot use reserved prefix "homey_": ${ids}`);
|
|
410
411
|
}
|
|
411
|
-
|
|
412
|
-
capabilityOptions.values = [...canonicalValues, ...extraValues];
|
|
413
412
|
}
|
|
414
413
|
|
|
415
414
|
// validate target_power exclude range must include 0
|
|
@@ -644,6 +643,88 @@ class App {
|
|
|
644
643
|
if (driver.matter && !(driver.connectivity && driver.connectivity.includes('matter'))) {
|
|
645
644
|
throw new Error(`drivers.${driver.id} Matter drivers require 'connectivity' to include 'matter'.`);
|
|
646
645
|
}
|
|
646
|
+
|
|
647
|
+
// Validate driver.firmwareUpdates
|
|
648
|
+
if (driver.firmwareUpdates && Array.isArray(driver.firmwareUpdates.updates)) {
|
|
649
|
+
// TODO: Remove when we want to roll out firmware updates.
|
|
650
|
+
if (level !== 'debug') {
|
|
651
|
+
throw new Error(`drivers.${driver.id} firmwareUpdates can only be included in debug mode validation.`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const isZigbeeDriver = typeof driver.zigbee === 'object' && driver.zigbee !== null;
|
|
655
|
+
|
|
656
|
+
if (!isZigbeeDriver) {
|
|
657
|
+
throw new Error(`drivers.${driver.id} firmwareUpdates are only supported for Zigbee drivers (missing driver.zigbee)`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
for (const [updateIndex, update] of Object.entries(driver.firmwareUpdates.updates)) {
|
|
661
|
+
const hasChangelogString = typeof update.changelog === 'string';
|
|
662
|
+
const hasChangelogEnString = update.changelog && typeof update.changelog.en === 'string';
|
|
663
|
+
|
|
664
|
+
if (!hasChangelogString && !hasChangelogEnString) {
|
|
665
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}] is missing a changelog string`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (!Array.isArray(update.files) || update.files.length === 0) {
|
|
669
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}].files must include at least one file`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (isZigbeeDriver) {
|
|
673
|
+
const driverManufacturerNames = toArray(driver.zigbee.manufacturerName);
|
|
674
|
+
const driverProductIds = toArray(driver.zigbee.productId);
|
|
675
|
+
|
|
676
|
+
const updateManufacturerNames = toArray(update.device.manufacturerName);
|
|
677
|
+
const updateProductIds = toArray(update.device.productId);
|
|
678
|
+
|
|
679
|
+
if (updateManufacturerNames.length === 0 || updateProductIds.length === 0) {
|
|
680
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}] must include at least one manufacturerName and productId`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const hasMismatchedManufacturerName = updateManufacturerNames.some(name => !driverManufacturerNames.includes(name));
|
|
684
|
+
const hasMismatchedProductId = updateProductIds.some(id => !driverProductIds.includes(id));
|
|
685
|
+
|
|
686
|
+
if (hasMismatchedManufacturerName) {
|
|
687
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}] has a manufacturerName that does not match the driver zigbee.manufacturerName`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (hasMismatchedProductId) {
|
|
691
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}] has a productId that does not match the driver zigbee.productId`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const seenFiles = new Set();
|
|
696
|
+
|
|
697
|
+
for (const [fileIndex, file] of Object.entries(update.files)) {
|
|
698
|
+
const relativeFilePath = Util.getOTAFilePath({ appPath: '', driverId: driver.id, fileName: file.name });
|
|
699
|
+
await this._ensureFileExistsCaseSensitive(relativeFilePath);
|
|
700
|
+
const filePath = join(this._path, relativeFilePath);
|
|
701
|
+
const isIntegrityValid = await Util.validateIntegrity(filePath, file.integrity);
|
|
702
|
+
|
|
703
|
+
if (!isIntegrityValid) {
|
|
704
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}].files[${fileIndex}] integrity mismatch`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (isZigbeeDriver) {
|
|
708
|
+
try {
|
|
709
|
+
await Util.validateZigbeeOTAHeader({
|
|
710
|
+
filePath,
|
|
711
|
+
manufacturerCode: file.manufacturerCode,
|
|
712
|
+
fileVersion: file.fileVersion,
|
|
713
|
+
imageType: file.imageType,
|
|
714
|
+
});
|
|
715
|
+
} catch (err) {
|
|
716
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}].files[${fileIndex}] invalid Zigbee OTA header: ${err.message}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const fileId = `${file.manufacturerCode}-${file.imageType}-${file.fileVersion}`;
|
|
721
|
+
if (seenFiles.has(fileId)) {
|
|
722
|
+
throw new Error(`drivers.${driver.id}.firmwareUpdates.updates[${updateIndex}] has multiple files with the same manufacturerCode, imageType and fileVersion`);
|
|
723
|
+
}
|
|
724
|
+
seenFiles.add(fileId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
647
728
|
}
|
|
648
729
|
|
|
649
730
|
const allDriversMatter = appJson.drivers.every(driver => driver.connectivity && driver.connectivity.includes('matter'));
|
package/lib/Util/file.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const ALLOWED_INTEGRITY_HASHES = new Set([
|
|
8
|
+
'blake2b512',
|
|
9
|
+
'blake2s256',
|
|
10
|
+
'sha256',
|
|
11
|
+
'sha384',
|
|
12
|
+
'sha512',
|
|
13
|
+
'sha512-256',
|
|
14
|
+
'sha3-256',
|
|
15
|
+
'sha3-384',
|
|
16
|
+
'sha3-512',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate a hex hash for the contents of a file.
|
|
21
|
+
* @param {string} filePath - Absolute or relative path to the file.
|
|
22
|
+
* @param {string} [hashName='sha256'] - Hash algorithm name (e.g. sha256).
|
|
23
|
+
* @returns {Promise<string>} Resolves with a hex-encoded digest.
|
|
24
|
+
*/
|
|
25
|
+
async function hashFile(filePath, hashName = 'sha256') {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const hash = crypto.createHash(hashName);
|
|
28
|
+
const stream = fs.createReadStream(filePath);
|
|
29
|
+
|
|
30
|
+
stream.on('error', reject);
|
|
31
|
+
stream.on('data', chunk => hash.update(chunk));
|
|
32
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate file integrity using a `hashName:hexDigest` string.
|
|
38
|
+
* @param {string} filePath - Absolute or relative path to the file.
|
|
39
|
+
* @param {string} integrity - Hash name and hex digest separated by `:`.
|
|
40
|
+
* @returns {Promise<boolean>} Resolves with true when hashes match.
|
|
41
|
+
*/
|
|
42
|
+
async function validateIntegrity(filePath, integrity) {
|
|
43
|
+
if (typeof integrity !== 'string') {
|
|
44
|
+
throw new TypeError('Integrity must be a string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parts = integrity.split(':');
|
|
48
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
49
|
+
throw new Error('Integrity must be in the format "hashName:hexDigest"');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [hashName, expectedHex] = parts;
|
|
53
|
+
const actualHex = await hashFile(filePath, hashName);
|
|
54
|
+
|
|
55
|
+
return actualHex === expectedHex.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create an integrity string for a file using an allowed hash algorithm.
|
|
60
|
+
* @param {string} filePath - Absolute or relative path to the file.
|
|
61
|
+
* @param {string} [hashName='sha256'] - Allowed hash algorithm name.
|
|
62
|
+
* @returns {Promise<string>} Resolves with `hashName:hexDigest`.
|
|
63
|
+
*/
|
|
64
|
+
async function getIntegrity(filePath, hashName = 'sha256') {
|
|
65
|
+
const normalizedHash = String(hashName || '').toLowerCase();
|
|
66
|
+
|
|
67
|
+
if (!ALLOWED_INTEGRITY_HASHES.has(normalizedHash)) {
|
|
68
|
+
throw new Error('Hash algorithm is not allowed for integrity checks');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const digest = await hashFile(filePath, normalizedHash);
|
|
72
|
+
return `${normalizedHash}:${digest}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getOTAFilePath({ appPath, driverId, fileName }) {
|
|
76
|
+
return path.join(appPath, 'drivers', driverId, 'assets', 'firmware', fileName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
hashFile,
|
|
81
|
+
getIntegrity,
|
|
82
|
+
validateIntegrity,
|
|
83
|
+
getOTAFilePath,
|
|
84
|
+
};
|
package/lib/Util/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fileUtils = require('./file');
|
|
4
|
+
const zigbeeUtils = require('./zigbee');
|
|
5
|
+
|
|
3
6
|
class Util {
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -20,6 +23,65 @@ class Util {
|
|
|
20
23
|
return wantedFeatures.filter(feature => !features.includes(feature));
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Create an integrity string for a file using an allowed hash algorithm.
|
|
28
|
+
* NOTE: This method is only available in Node.js environments.
|
|
29
|
+
* @param {string} filePath - Absolute or relative path to the file.
|
|
30
|
+
* @param {string} hashName - Allowed hash algorithm name.
|
|
31
|
+
* @returns {Promise<string>} Resolves with `hashName:hexDigest`.
|
|
32
|
+
*/
|
|
33
|
+
static getIntegrity(filePath, hashName) {
|
|
34
|
+
return fileUtils.getIntegrity(filePath, hashName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate file integrity using a `hashName:hexDigest` string.
|
|
39
|
+
* NOTE: This method is only available in Node.js environments.
|
|
40
|
+
* @param {string} filePath - Absolute or relative path to the file.
|
|
41
|
+
* @param {string} integrity - Hash name and hex digest separated by `:`.
|
|
42
|
+
* @returns {Promise<boolean>} Resolves with true when hashes match.
|
|
43
|
+
*/
|
|
44
|
+
static validateIntegrity(filePath, integrity) {
|
|
45
|
+
return fileUtils.validateIntegrity(filePath, integrity);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse the Zigbee OTA header from a file.
|
|
50
|
+
* NOTE: This method is only available in Node.js environments.
|
|
51
|
+
* @param {string} filePath - Absolute or relative path to the OTA file.
|
|
52
|
+
* @returns {Promise<import('./zigbee').ZigbeeOTAHeader>} Parsed header values.
|
|
53
|
+
*/
|
|
54
|
+
static parseZigbeeOTAHeader(filePath) {
|
|
55
|
+
return zigbeeUtils.parseZigbeeOTAHeader(filePath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate a Zigbee OTA header against expected values.
|
|
60
|
+
* NOTE: This method is only available in Node.js environments.
|
|
61
|
+
* @param {object} options - Validation options.
|
|
62
|
+
* @param {string} options.filePath - Absolute or relative path to the OTA file.
|
|
63
|
+
* @param {number} [options.manufacturerCode] - Expected manufacturer code.
|
|
64
|
+
* @param {number} [options.fileVersion] - Expected file version.
|
|
65
|
+
* @param {number} [options.imageType] - Expected image type.
|
|
66
|
+
* @returns {Promise<import('./zigbee').ZigbeeOTAHeader>} Parsed header values.
|
|
67
|
+
*/
|
|
68
|
+
static validateZigbeeOTAHeader(options) {
|
|
69
|
+
return zigbeeUtils.validateZigbeeOTAHeader(options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the expected file path for an OTA file based on app path, driver ID and file name.
|
|
74
|
+
* NOTE: This method is only available in Node.js environments.
|
|
75
|
+
* @param {object} options - File path options.
|
|
76
|
+
* @param {string} options.appPath - Absolute path to the app.
|
|
77
|
+
* @param {string} options.driverId - Driver ID.
|
|
78
|
+
* @param {string} options.fileName - OTA file name.
|
|
79
|
+
* @returns {string} Resolves with the expected file path.
|
|
80
|
+
*/
|
|
81
|
+
static getOTAFilePath({ appPath, driverId, fileName }) {
|
|
82
|
+
return fileUtils.getOTAFilePath({ appPath, driverId, fileName });
|
|
83
|
+
}
|
|
84
|
+
|
|
23
85
|
}
|
|
24
86
|
|
|
25
87
|
Util.FEATURE_SPEAKER = 'speaker';
|