@homebridge-wyze-node24/homebridge-wyze-node24 1.0.4 → 1.1.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/README.md CHANGED
@@ -26,7 +26,7 @@ After you have done that if you feel like my work has been valuable to you I wel
26
26
  For more information about our version updates, please check our [change log](CHANGELOG.md).
27
27
 
28
28
  ## Requirements
29
- - Node.js 18.20.4, 20.15.1, or 24.13.1
29
+ - Node.js 24.13.1+
30
30
  - Homebridge 1.6.0+ (or 2.0.0 beta)
31
31
 
32
32
  ## Configuration
@@ -63,7 +63,7 @@ Supported devices will be discovered and added to Homebridge automatically.
63
63
  * **`apiKey`** – Navigate to [this portal](https://developer-api-console.wyze.com/)
64
64
  * **`keyId`** – Navigate to [this portal](https://developer-api-console.wyze.com/), and click Login to sign in to your Wyze account.
65
65
  Note: Ensure that the login info you are using matches the info you use when logLevel into the Wyze app.
66
- Once youve signed in, youll be automatically redirected back to the developer page.
66
+ Once you've signed in, you'll be automatically redirected back to the developer page.
67
67
  Click Create an API key for your API key to be created.
68
68
  Once created, you can click view to see the entire key.
69
69
  You should receive an email that a new API key has been generated.
@@ -95,4 +95,4 @@ Special thanks to the following projects for reference and inspiration:
95
95
 
96
96
  Thanks to [misenhower](https://github.com/misenhower/homebridge-wyze-connected-home) for the original Wyze Homebridge plugin, and thanks to [contributors](https://github.com/misenhower/homebridge-wyze-connected-home/graphs/contributors) and [other developers who were not merged](https://github.com/misenhower/homebridge-wyze-connected-home/pulls) for volunteering their time to help fix bugs and add support for more devices and features.
97
97
 
98
- This plugin is an actively maintained fork of misenhower's original [Wyze Homebridge Plugin](https://github.com/misenhower/homebridge-wyze-connected-home) project.
98
+ This plugin is an actively maintained fork of misenhower's original [Wyze Homebridge Plugin](https://github.com/misenhower/homebridge-wyze-connected-home) project.
@@ -16,30 +16,43 @@
16
16
  },
17
17
  "username": {
18
18
  "title": "Username (E-Mail Address)",
19
- "description": "The e-mail address used for your Wyze account",
19
+ "description": "The e-mail address used for your Wyze account. You can also set WYZE_USERNAME env var or use secretsFile.",
20
20
  "type": "string",
21
21
  "default": "",
22
22
  "required": true
23
23
  },
24
24
  "password": {
25
25
  "title": "Password",
26
- "description": "The password used for your Wyze account",
26
+ "description": "The password used for your Wyze account. You can also set WYZE_PASSWORD env var or use secretsFile.",
27
27
  "type": "string",
28
+ "format": "password",
28
29
  "default": "",
29
30
  "required": true
30
31
  },
31
32
  "keyId": {
32
33
  "title": "Key ID",
33
- "description": "API Key/ID available from the official Wyze Portal https://developer-api-console.wyze.com",
34
+ "description": "API Key/ID available from the official Wyze Portal https://developer-api-console.wyze.com. You can also set WYZE_KEY_ID env var or use secretsFile.",
34
35
  "type": "string",
36
+ "format": "password",
35
37
  "required": true
36
38
  },
37
39
  "apiKey": {
38
40
  "title": "API Key",
39
- "description": "API Key/ID available from the official Wyze Portal https://developer-api-console.wyze.com/)",
41
+ "description": "API Key/ID available from the official Wyze Portal https://developer-api-console.wyze.com. You can also set WYZE_API_KEY env var or use secretsFile.",
40
42
  "type": "string",
43
+ "format": "password",
41
44
  "required": true
42
45
  },
46
+ "secretsFile": {
47
+ "title": "Secrets file path (optional)",
48
+ "description": "Path to a local JSON file containing secrets (e.g., username/password/apiKey/etc). File must be chmod 600 (owner-only) or the plugin will refuse to start.",
49
+ "type": "string",
50
+ "default": "",
51
+ "required": false,
52
+ "condition": {
53
+ "functionBody": "return model.showAdvancedOptions === true;"
54
+ }
55
+ },
43
56
  "refreshInterval": {
44
57
  "title": "Refresh Interval",
45
58
  "description": "Specify the number of milliseconds to wait between updates, default is 60000 ms (60 seconds)",
@@ -63,7 +76,7 @@
63
76
  },
64
77
  "apiLogEnabled": {
65
78
  "title": "Enable API logging",
66
- "description": "",
79
+ "description": "WARNING: may log request/response details from upstream libraries. Keep OFF unless you understand the risk.",
67
80
  "type": "boolean",
68
81
  "default": false,
69
82
  "condition": {
@@ -79,6 +92,15 @@
79
92
  "functionBody": "return model.showAdvancedOptions === true;"
80
93
  }
81
94
  },
95
+ "dangerouslyAllowCustomBaseUrls": {
96
+ "title": "Dangerously allow custom API base URLs",
97
+ "description": "SECURITY WARNING: If enabled, the plugin will accept authBaseUrl/apiBaseUrl overrides. This can enable SSRF and credential exfiltration. Only enable if you know exactly what you're doing.",
98
+ "type": "boolean",
99
+ "default": false,
100
+ "condition": {
101
+ "functionBody": "return model.showAdvancedOptions === true;"
102
+ }
103
+ },
82
104
  "lowBatteryPercentage": {
83
105
  "title": "Low Battery Percentage",
84
106
  "description": "Specify the Percentage of battery when consider low, default is 30%",
@@ -204,4 +226,4 @@
204
226
  },
205
227
  "form": null,
206
228
  "display": null
207
- }
229
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@homebridge-wyze-node24/homebridge-wyze-node24",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Wyze Smart Home plugin for Homebridge compatible with Node.JS 24",
5
5
  "license": "MIT",
6
+ "type": "commonjs",
6
7
  "main": "src/index.js",
7
8
  "scripts": {
8
9
  "lint": "eslint src/**.js --max-warnings=0"
@@ -22,8 +23,17 @@
22
23
  },
23
24
  "engines": {
24
25
  "homebridge": "^1.6.0 || ^2.0.0-beta.0",
25
- "node": "^18.20.4 || ^20.15.1 || ^24.13.1"
26
+ "node": ">=24.13.1"
26
27
  },
28
+ "files": [
29
+ "src/**/*",
30
+ "config.schema.json",
31
+ "configSample.json",
32
+ "README.md",
33
+ "CHANGELOG.md",
34
+ "license.txt",
35
+ "logo.png"
36
+ ],
27
37
  "dependencies": {
28
38
  "aws-sdk": "2.1693.0",
29
39
  "axios": "^1.5.0",
@@ -3,6 +3,13 @@ const { OutdoorPlugModels, PlugModels, CommonModels, CameraModels, LeakSensorMod
3
3
  TemperatureHumidityModels, LockModels, MotionSensorModels, ContactSensorModels, LightModels,
4
4
  LightStripModels, MeshLightModels, ThermostatModels, S1GatewayModels } = require('./enums')
5
5
 
6
+ const {
7
+ getValidatedBaseUrls,
8
+ resolveSecrets,
9
+ sanitizeDeviceName,
10
+ wrapLogger
11
+ } = require('./security')
12
+
6
13
  const WyzeAPI = require('wyze-api') // Uncomment for Release
7
14
  //const WyzeAPI = require('./wyze-api/src') // Comment for Release
8
15
  const WyzePlug = require('./accessories/WyzePlug')
@@ -29,8 +36,8 @@ function delay(ms) {
29
36
 
30
37
  module.exports = class WyzeSmartHome {
31
38
  constructor(log, config, api) {
32
- this.log = log
33
- this.config = config
39
+ this.log = wrapLogger(log)
40
+ this.config = resolveSecrets(config || {}, this.log)
34
41
  this.api = api
35
42
  this.client = this.getClient()
36
43
 
@@ -44,6 +51,8 @@ module.exports = class WyzeSmartHome {
44
51
  }
45
52
 
46
53
  getClient() {
54
+ const { authBaseUrl, apiBaseUrl } = getValidatedBaseUrls(this.config, this.log)
55
+
47
56
  return new WyzeAPI({
48
57
  // User login parameters
49
58
  username: this.config.username,
@@ -57,9 +66,9 @@ module.exports = class WyzeSmartHome {
57
66
  lowBatteryPercentage: this.config.lowBatteryPercentage,
58
67
  //Storage Path
59
68
  persistPath: homebridge.user.persistPath(),
60
- //URLs
61
- authBaseUrl: this.config.authBaseUrl,
62
- apiBaseUrl: this.config.apiBaseUrl,
69
+ //URLs (strictly validated)
70
+ authBaseUrl,
71
+ apiBaseUrl,
63
72
  // App emulation constants
64
73
  authApiKey: this.config.authApiKey,
65
74
  phoneId: this.config.phoneId,
@@ -83,14 +92,24 @@ module.exports = class WyzeSmartHome {
83
92
  }
84
93
 
85
94
  async runLoop() {
86
- const interval = this.config.refreshInterval || DEFAULT_REFRESH_INTERVAL
95
+ const baseInterval = this.config.refreshInterval || DEFAULT_REFRESH_INTERVAL
96
+ let failures = 0
97
+
87
98
  // eslint-disable-next-line no-constant-condition
88
99
  while (true) {
89
100
  try {
90
101
  await this.refreshDevices()
91
- } catch (e) { }
102
+ failures = 0
103
+ } catch (e) {
104
+ failures += 1
105
+ const message = e?.message || String(e)
106
+ this.log.error(`Refresh loop error: ${message}`)
107
+ }
92
108
 
93
- await delay(interval)
109
+ // simple backoff to avoid noisy retry loops on auth/network failures
110
+ const backoffMultiplier = Math.min(6, failures) // caps at 64x
111
+ const delayMs = baseInterval * (failures === 0 ? 1 : Math.pow(2, backoffMultiplier))
112
+ await delay(delayMs)
94
113
  }
95
114
  }
96
115
 
@@ -102,10 +121,10 @@ module.exports = class WyzeSmartHome {
102
121
  const timestamp = objectList.ts
103
122
  const devices = objectList.data.device_list
104
123
 
105
- if (this.config.pluginLoggingEnabled) this.log(`Found ${devices.length} device(s)`)
124
+ if (this.config.pluginLoggingEnabled) this.log(`Found ${devices.length} device(s)`)
106
125
  await this.loadDevices(devices, timestamp)
107
126
  } catch (e) {
108
- this.log.error(`Error getting devices: ${e}`)
127
+ this.log.error(`Error getting devices: ${e?.message || e}`)
109
128
  throw e
110
129
  }
111
130
  }
@@ -122,7 +141,7 @@ module.exports = class WyzeSmartHome {
122
141
 
123
142
  const removedAccessories = this.accessories.filter(a => !foundAccessories.includes(a))
124
143
  if (removedAccessories.length > 0) {
125
- if (this.config.pluginLoggingEnabled) this.log(`Removing ${removedAccessories.length} device(s)`)
144
+ if (this.config.pluginLoggingEnabled) this.log(`Removing ${removedAccessories.length} device(s)`)
126
145
  const removedHomeKitAccessories = removedAccessories.map(a => a.homeKitAccessory)
127
146
  this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, removedHomeKitAccessories)
128
147
  }
@@ -131,28 +150,30 @@ module.exports = class WyzeSmartHome {
131
150
  }
132
151
 
133
152
  async loadDevice(device, timestamp) {
134
- const accessoryClass = this.getAccessoryClass(device.product_type, device.product_model, device.mac, device.nickname)
153
+ const safeNickname = sanitizeDeviceName(device.nickname)
154
+
155
+ const accessoryClass = this.getAccessoryClass(device.product_type, device.product_model, device.mac, safeNickname)
135
156
  if (!accessoryClass) {
136
- if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Unsupported device type: (Name: ${device.nickname}) (MAC: ${device.mac}) (Model: ${device.product_model})`)
157
+ if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Unsupported device type: (Name: ${safeNickname}) (MAC: ${device.mac}) (Model: ${device.product_model})`)
137
158
  return
138
159
  }
139
160
  else if (this.config.filterByMacAddressList?.find(d => d === device.mac) || this.config.filterDeviceTypeList?.find(d => d === device.product_type)) {
140
- if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Ignoring (${device.nickname}) (MAC: ${device.mac}) because it is in the Ignore Device list`)
161
+ if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Ignoring (${safeNickname}) (MAC: ${device.mac}) because it is in the Ignore Device list`)
141
162
  return
142
163
  }
143
164
  else if (device.product_type == 'S1Gateway' && this.config.hms == false) {
144
- if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Ignoring (${device.nickname}) (MAC: ${device.mac}) because it is not enabled`)
165
+ if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Ignoring (${safeNickname}) (MAC: ${device.mac}) because it is not enabled`)
145
166
  return
146
167
  }
147
168
 
148
169
 
149
170
  let accessory = this.accessories.find(a => a.matches(device))
150
171
  if (!accessory) {
151
- const homeKitAccessory = this.createHomeKitAccessory(device)
172
+ const homeKitAccessory = this.createHomeKitAccessory(device, safeNickname)
152
173
  accessory = new accessoryClass(this, homeKitAccessory)
153
174
  this.accessories.push(accessory)
154
175
  } else {
155
- if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Loading accessory from cache ${device.nickname} (MAC: ${device.mac})`)
176
+ if (this.config.pluginLoggingEnabled) this.log(`[${device.product_type}] Loading accessory from cache ${safeNickname} (MAC: ${device.mac})`)
156
177
  }
157
178
  accessory.update(device, timestamp)
158
179
 
@@ -192,16 +213,16 @@ module.exports = class WyzeSmartHome {
192
213
  }
193
214
  }
194
215
 
195
- createHomeKitAccessory(device) {
216
+ createHomeKitAccessory(device, safeNickname) {
196
217
  const uuid = UUIDGen.generate(device.mac)
197
218
 
198
- const homeKitAccessory = new Accessory(device.nickname, uuid)
219
+ const homeKitAccessory = new Accessory(safeNickname, uuid)
199
220
 
200
221
  homeKitAccessory.context = {
201
222
  mac: device.mac,
202
223
  product_type: device.product_type,
203
224
  product_model: device.product_model,
204
- nickname: device.nickname
225
+ nickname: safeNickname
205
226
  }
206
227
 
207
228
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [homeKitAccessory])
@@ -224,7 +245,8 @@ module.exports = class WyzeSmartHome {
224
245
  try {
225
246
  this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [homeKitAccessory])
226
247
  } catch (error) {
227
- this.log.error(`Error removing accessory ${homeKitAccessory.context.nickname} (MAC: ${homeKitAccessory.context.mac}) : ${error}`)
248
+ const safeName = sanitizeDeviceName(homeKitAccessory.context.nickname)
249
+ this.log.error(`Error removing accessory ${safeName} (MAC: ${homeKitAccessory.context.mac}) : ${error?.message || error}`)
228
250
  }
229
251
  }
230
252
  }
@@ -1,4 +1,5 @@
1
1
  const { Service, Characteristic } = require("../types");
2
+ const { sanitizeDeviceName } = require("../security");
2
3
 
3
4
  // Responses from the Wyze API can lag a little after a new value is set
4
5
  const UPDATE_THROTTLE_MS = 1000;
@@ -33,6 +34,7 @@ module.exports = class WyzeAccessory {
33
34
 
34
35
  async update(device, timestamp) {
35
36
  const productType = device.product_type;
37
+ const safeNickname = sanitizeDeviceName(device.nickname);
36
38
 
37
39
  switch (productType) {
38
40
  default:
@@ -40,14 +42,14 @@ module.exports = class WyzeAccessory {
40
42
  mac: device.mac,
41
43
  product_type: device.product_type,
42
44
  product_model: device.product_model,
43
- nickname: device.nickname,
45
+ nickname: safeNickname,
44
46
  };
45
47
  break;
46
48
  }
47
49
 
48
50
  this.homeKitAccessory
49
51
  .getService(Service.AccessoryInformation)
50
- .updateCharacteristic(Characteristic.Name, device.nickname)
52
+ .updateCharacteristic(Characteristic.Name, safeNickname)
51
53
  .updateCharacteristic(Characteristic.Manufacturer, "Wyze")
52
54
  .updateCharacteristic(Characteristic.Model, device.product_model)
53
55
  .updateCharacteristic(Characteristic.SerialNumber, device.mac)
@@ -82,4 +84,4 @@ module.exports = class WyzeAccessory {
82
84
  sleep(ms) {
83
85
  return new Promise((resolve) => setTimeout(resolve, ms * 1000));
84
86
  }
85
- };
87
+ };
@@ -81,7 +81,12 @@ module.exports = class WyzeMeshLight extends WyzeAccessory {
81
81
  ) {
82
82
  return true;
83
83
  } else {
84
- this.plugin.log(`Encountered invalid property value: ${JSON.stringify(property, null, 2)}`);
84
+ // Avoid logging raw API objects; they can contain device IDs / other sensitive fields.
85
+ if (this.plugin.config.pluginLoggingEnabled) {
86
+ this.plugin.log(
87
+ `Encountered invalid property value (pid=${property?.pid}, value=${String(property?.value)})`
88
+ );
89
+ }
85
90
  return false;
86
91
  }
87
92
  }
@@ -288,4 +293,4 @@ module.exports = class WyzeMeshLight extends WyzeAccessory {
288
293
  }
289
294
  }
290
295
  }
291
- };
296
+ };
@@ -0,0 +1,255 @@
1
+ const fs = require('fs')
2
+ const net = require('net')
3
+
4
+ const DEFAULT_AUTH_BASE_URL = 'https://auth-prod.api.wyze.com'
5
+ const DEFAULT_API_BASE_URL = 'https://api.wyzecam.com'
6
+
7
+ const WYZE_ALLOWED_HOSTNAMES = new Set([
8
+ new URL(DEFAULT_AUTH_BASE_URL).hostname,
9
+ new URL(DEFAULT_API_BASE_URL).hostname
10
+ ])
11
+
12
+ const SECRET_KEYS = [
13
+ 'username',
14
+ 'password',
15
+ 'mfaCode',
16
+ 'keyId',
17
+ 'apiKey',
18
+ 'authApiKey',
19
+ 'phoneId',
20
+ 'appName',
21
+ 'appVer',
22
+ 'appVersion',
23
+ 'userAgent',
24
+ 'sc',
25
+ 'sv',
26
+ 'fordAppKey',
27
+ 'fordAppSecret',
28
+ 'oliveSigningSecret',
29
+ 'oliveAppId',
30
+ 'appInfo'
31
+ ]
32
+
33
+ const ENV_MAP = {
34
+ username: 'WYZE_USERNAME',
35
+ password: 'WYZE_PASSWORD',
36
+ mfaCode: 'WYZE_MFA_CODE',
37
+ keyId: 'WYZE_KEY_ID',
38
+ apiKey: 'WYZE_API_KEY',
39
+ authApiKey: 'WYZE_AUTH_API_KEY',
40
+ phoneId: 'WYZE_PHONE_ID',
41
+ appName: 'WYZE_APP_NAME',
42
+ appVer: 'WYZE_APP_VER',
43
+ appVersion: 'WYZE_APP_VERSION',
44
+ userAgent: 'WYZE_USER_AGENT',
45
+ sc: 'WYZE_SC',
46
+ sv: 'WYZE_SV',
47
+ fordAppKey: 'WYZE_FORD_APP_KEY',
48
+ fordAppSecret: 'WYZE_FORD_APP_SECRET',
49
+ oliveSigningSecret: 'WYZE_OLIVE_SIGNING_SECRET',
50
+ oliveAppId: 'WYZE_OLIVE_APP_ID',
51
+ appInfo: 'WYZE_APP_INFO'
52
+ }
53
+
54
+ function stripControlChars(input) {
55
+ return String(input)
56
+ .replace(/[\r\n\t]+/g, ' ')
57
+ .replace(/[\u0000-\u001F\u007F]/g, '')
58
+ }
59
+
60
+ function boundLength(input, maxLen) {
61
+ const s = String(input)
62
+ if (s.length <= maxLen) return s
63
+ return s.slice(0, maxLen) + '…'
64
+ }
65
+
66
+ function redactMacs(str) {
67
+ return str.replace(/\b([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\b/g, (mac) => {
68
+ const parts = mac.split(':')
69
+ return 'xx:xx:xx:xx:xx:' + parts[5]
70
+ })
71
+ }
72
+
73
+ function redactBearerTokens(str) {
74
+ return str.replace(/\bBearer\s+[-._~+/0-9a-zA-Z]+=*\b/g, 'Bearer [REDACTED]')
75
+ }
76
+
77
+ function redactKeyValueSecrets(str) {
78
+ const keys = [
79
+ 'password',
80
+ 'apiKey',
81
+ 'keyId',
82
+ 'access_token',
83
+ 'refresh_token',
84
+ 'accessToken',
85
+ 'refreshToken',
86
+ 'authorization',
87
+ 'fordAppSecret',
88
+ 'fordAppKey',
89
+ 'oliveSigningSecret',
90
+ 'oliveAppId',
91
+ 'authApiKey'
92
+ ]
93
+
94
+ const keyGroup = keys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
95
+
96
+ // Covers JSON-ish and plain logs like `password: ...` or `"password":"..."`
97
+ const re = new RegExp(`(\\b(?:${keyGroup})\\b"?\\s*[:=]\\s*)("?)([^"\\s,}]+)("?)`, 'gi')
98
+ return str.replace(re, (_m, prefix) => `${prefix}[REDACTED]`)
99
+ }
100
+
101
+ function sanitizeLogMessage(input) {
102
+ let s = stripControlChars(input)
103
+ s = redactBearerTokens(s)
104
+ s = redactKeyValueSecrets(s)
105
+ s = redactMacs(s)
106
+ return boundLength(s, 2000)
107
+ }
108
+
109
+ function sanitizeDeviceName(name) {
110
+ return boundLength(stripControlChars(name).trim(), 64) || 'Wyze Device'
111
+ }
112
+
113
+ function wrapLogger(log) {
114
+ const baseFn = typeof log === 'function' ? log : (...args) => log.info(...args)
115
+
116
+ const wrapped = (...args) => baseFn(...args.map(sanitizeLogMessage))
117
+
118
+ for (const level of ['error', 'warn', 'info', 'debug']) {
119
+ const underlying = log?.[level]
120
+ if (typeof underlying === 'function') {
121
+ wrapped[level] = (...args) => underlying(...args.map(sanitizeLogMessage))
122
+ }
123
+ }
124
+
125
+ return wrapped
126
+ }
127
+
128
+ function loadSecretsFromFile(secretsFile, log) {
129
+ const stat = fs.statSync(secretsFile)
130
+ // Must not be readable/writable by group/others
131
+ if ((stat.mode & 0o077) !== 0) {
132
+ throw new Error(
133
+ `Secrets file permissions too open: ${secretsFile}. ` +
134
+ 'Set mode to 600 (owner read/write only).'
135
+ )
136
+ }
137
+
138
+ const raw = fs.readFileSync(secretsFile, 'utf8')
139
+ const parsed = JSON.parse(raw)
140
+ return parsed && typeof parsed === 'object' ? parsed : {}
141
+ }
142
+
143
+ function resolveSecrets(config, log) {
144
+ const merged = { ...config }
145
+
146
+ if (config?.secretsFile) {
147
+ try {
148
+ const fromFile = loadSecretsFromFile(config.secretsFile, log)
149
+ for (const k of SECRET_KEYS) {
150
+ if (fromFile[k] != null && fromFile[k] !== '') merged[k] = fromFile[k]
151
+ }
152
+ log('Loaded secrets from secretsFile')
153
+ } catch (e) {
154
+ // Refuse to start: this prevents accidentally using a world-readable secrets file.
155
+ log.error(`Failed to load secretsFile: ${e?.message || e}`)
156
+ throw e
157
+ }
158
+ }
159
+
160
+ for (const [key, envName] of Object.entries(ENV_MAP)) {
161
+ const v = process.env[envName]
162
+ if (v == null || v === '') continue
163
+
164
+ if (key === 'appInfo') {
165
+ try {
166
+ merged.appInfo = JSON.parse(v)
167
+ } catch {
168
+ merged.appInfo = v
169
+ }
170
+ continue
171
+ }
172
+
173
+ merged[key] = v
174
+ }
175
+
176
+ return merged
177
+ }
178
+
179
+ function isIpLiteral(hostname) {
180
+ return net.isIP(hostname) !== 0
181
+ }
182
+
183
+ function isClearlyLocalHostname(hostname) {
184
+ const lower = String(hostname).toLowerCase()
185
+ return (
186
+ lower === 'localhost' ||
187
+ lower.endsWith('.localhost') ||
188
+ lower.endsWith('.local')
189
+ )
190
+ }
191
+
192
+ function validateBaseUrl(urlString) {
193
+ const u = new URL(urlString)
194
+ if (u.protocol !== 'https:') {
195
+ throw new Error(`Base URL must use https: ${urlString}`)
196
+ }
197
+ if (isIpLiteral(u.hostname)) {
198
+ throw new Error(`IP literals are not allowed in base URLs: ${urlString}`)
199
+ }
200
+ if (isClearlyLocalHostname(u.hostname)) {
201
+ throw new Error(`Local hostnames are not allowed in base URLs: ${urlString}`)
202
+ }
203
+ return u
204
+ }
205
+
206
+ function getValidatedBaseUrls(config, log) {
207
+ const hasCustom = Boolean(config?.authBaseUrl || config?.apiBaseUrl)
208
+ const allowCustom = Boolean(config?.dangerouslyAllowCustomBaseUrls)
209
+
210
+ if (hasCustom && !allowCustom) {
211
+ log.error(
212
+ 'Custom authBaseUrl/apiBaseUrl are ignored for security. ' +
213
+ 'If you understand the risks and still want this, set dangerouslyAllowCustomBaseUrls=true.'
214
+ )
215
+ return {
216
+ authBaseUrl: DEFAULT_AUTH_BASE_URL,
217
+ apiBaseUrl: DEFAULT_API_BASE_URL
218
+ }
219
+ }
220
+
221
+ if (!hasCustom) {
222
+ return {
223
+ authBaseUrl: DEFAULT_AUTH_BASE_URL,
224
+ apiBaseUrl: DEFAULT_API_BASE_URL
225
+ }
226
+ }
227
+
228
+ // Custom base URLs only when explicitly opted-in.
229
+ const authUrl = config.authBaseUrl ? validateBaseUrl(config.authBaseUrl) : new URL(DEFAULT_AUTH_BASE_URL)
230
+ const apiUrl = config.apiBaseUrl ? validateBaseUrl(config.apiBaseUrl) : new URL(DEFAULT_API_BASE_URL)
231
+
232
+ // Even when opted-in, refuse non-Wyze hosts by default.
233
+ // This keeps the escape-hatch useful for path tweaks, but blocks credential exfiltration/SSRF.
234
+ if (!WYZE_ALLOWED_HOSTNAMES.has(authUrl.hostname) || !WYZE_ALLOWED_HOSTNAMES.has(apiUrl.hostname)) {
235
+ throw new Error(
236
+ `Custom base URL hostnames must be one of: ${Array.from(WYZE_ALLOWED_HOSTNAMES).join(', ')}. ` +
237
+ 'Refusing to start.'
238
+ )
239
+ }
240
+
241
+ return {
242
+ authBaseUrl: authUrl.toString().replace(/\/+$/, ''),
243
+ apiBaseUrl: apiUrl.toString().replace(/\/+$/, '')
244
+ }
245
+ }
246
+
247
+ module.exports = {
248
+ DEFAULT_AUTH_BASE_URL,
249
+ DEFAULT_API_BASE_URL,
250
+ sanitizeLogMessage,
251
+ sanitizeDeviceName,
252
+ wrapLogger,
253
+ resolveSecrets,
254
+ getValidatedBaseUrls
255
+ }
package/.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto
@@ -1,44 +0,0 @@
1
- ---
2
- name: Bug Report
3
- about: Create a report to help us improve
4
- title: ''
5
- labels: bug, question
6
- assignees: ''
7
-
8
- ---
9
-
10
- <!-- You must use the issue template below when submitting a bug -->
11
-
12
- **Describe The Bug:**
13
- <!-- A clear and concise description of what the bug is. -->
14
-
15
- **To Reproduce:**
16
- <!-- Steps to reproduce the behavior. -->
17
-
18
- **Expected behavior:**
19
- <!-- A clear and concise description of what you expected to happen. -->
20
-
21
- **Logs:**
22
-
23
- ```
24
- Show the Homebridge logs here, remove any sensitive information.
25
- ```
26
-
27
- **Plugin Config:**
28
-
29
- ```json
30
- Show your Homebridge config.json here, remove any sensitive information.
31
- ```
32
-
33
- **Screenshots:**
34
- <!-- If applicable, add screenshots to help explain your problem. -->
35
-
36
- **Environment:**
37
-
38
- * **Plugin Version**:
39
- * **Homebridge Version**: <!-- homebridge -V -->
40
- * **Node.js Version**: <!-- node -v -->
41
- * **NPM Version**: <!-- npm -v -->
42
- * **Operating System**: <!-- Raspbian / Ubuntu / Debian / Windows / macOS / Docker / hb-service -->
43
-
44
- <!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->
@@ -1,5 +0,0 @@
1
- # blank_issues_enabled: false
2
- # contact_links:
3
- # - name: Homebridge Discord Community
4
- # url: https://discord.gg/kqNCe2D
5
- # about: Ask your questions in the #YOUR_CHANNEL_HERE channel
@@ -1,23 +0,0 @@
1
- ---
2
- name: Feature Request
3
- about: Suggest an idea for this project
4
- title: ''
5
- labels: enhancement
6
- assignees: ''
7
-
8
- ---
9
-
10
- **Is your feature request related to a problem? Please describe:**
11
- <!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
12
-
13
- **Describe the solution you'd like:**
14
- <!-- A clear and concise description of what you want to happen. -->
15
-
16
- **Describe alternatives you've considered:**
17
- <!-- A clear and concise description of any alternative solutions or features you've considered. -->
18
-
19
- **Additional context:**
20
- <!-- Add any other context or screenshots about the feature request here. -->
21
-
22
-
23
- <!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->
@@ -1,38 +0,0 @@
1
- ---
2
- name: Support Request
3
- about: Need help?
4
- title: ''
5
- labels: question
6
- assignees: ''
7
-
8
- ---
9
-
10
- <!-- You must use the issue template below when submitting a support request -->
11
-
12
- **Describe Your Problem:**
13
- <!-- A clear and concise description of what problem you are trying to solve. -->
14
-
15
- **Logs:**
16
-
17
- ```
18
- Show the Homebridge logs here, remove any sensitive information.
19
- ```
20
-
21
- **Plugin Config:**
22
-
23
- ```json
24
- Show your Homebridge config.json here, remove any sensitive information.
25
- ```
26
-
27
- **Screenshots:**
28
- <!-- If applicable, add screenshots to help explain your problem. -->
29
-
30
- **Environment:**
31
-
32
- * **Plugin Version**:
33
- * **Homebridge Version**: <!-- homebridge -V -->
34
- * **Node.js Version**: <!-- node -v -->
35
- * **NPM Version**: <!-- npm -v -->
36
- * **Operating System**: <!-- Raspbian / Ubuntu / Debian / Windows / macOS / Docker / hb-service -->
37
-
38
- <!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->
@@ -1,12 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: "npm"
4
- directory: "/"
5
- schedule:
6
- interval: "weekly"
7
- open-pull-requests-limit: 5
8
- rebase-strategy: "auto"
9
- commit-message:
10
- prefix: "deps"
11
- labels:
12
- - "dependencies"
@@ -1,72 +0,0 @@
1
- # For most projects, this workflow file will not need changing; you simply need
2
- # to commit it to your repository.
3
- #
4
- # You may wish to alter this file to override the set of languages analyzed,
5
- # or to provide custom queries or build logic.
6
- #
7
- # ******** NOTE ********
8
- # We have attempted to detect the languages in your repository. Please check
9
- # the `language` matrix defined below to confirm you have the correct set of
10
- # supported CodeQL languages.
11
- #
12
- name: "CodeQL"
13
-
14
- on:
15
- push:
16
- branches: [ "main" ]
17
- pull_request:
18
- # The branches below must be a subset of the branches above
19
- branches: [ "main" ]
20
- schedule:
21
- - cron: '36 13 * * 1'
22
-
23
- jobs:
24
- analyze:
25
- name: Analyze
26
- runs-on: ubuntu-latest
27
- permissions:
28
- actions: read
29
- contents: read
30
- security-events: write
31
-
32
- strategy:
33
- fail-fast: false
34
- matrix:
35
- language: [ 'Typescript' ]
36
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38
-
39
- steps:
40
- - name: Checkout repository
41
- uses: actions/checkout@v4
42
-
43
- # Initializes the CodeQL tools for scanning.
44
- - name: Initialize CodeQL
45
- uses: github/codeql-action/init@v4
46
- with:
47
- languages: ${{ matrix.language }}
48
- # If you wish to specify custom queries, you can do so here or in a config file.
49
- # By default, queries listed here will override any specified in a config file.
50
- # Prefix the list here with "+" to use these queries and those in the config file.
51
-
52
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53
- # queries: security-extended,security-and-quality
54
-
55
-
56
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57
- # If this step fails, then you should remove it and run the build manually (see below)
58
- - name: Autobuild
59
- uses: github/codeql-action/autobuild@v4
60
-
61
- # ℹ️ Command-line programs to run using the OS shell.
62
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63
-
64
- # If the Autobuild fails above, remove it and uncomment the following three lines.
65
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66
-
67
- # - run: |
68
- # echo "Run, Build Application using script"
69
- # ./location_of_script_within_repo/buildscript.sh
70
-
71
- - name: Perform CodeQL Analysis
72
- uses: github/codeql-action/analyze@v4
@@ -1,16 +0,0 @@
1
- name: Greetings
2
-
3
- on: [pull_request_target, issues]
4
-
5
- jobs:
6
- greeting:
7
- runs-on: ubuntu-latest
8
- permissions:
9
- issues: write
10
- pull-requests: write
11
- steps:
12
- - uses: actions/first-interaction@v1
13
- with:
14
- repo-token: ${{ secrets.GITHUB_TOKEN }}
15
- issue-message: "Message that will be displayed on users' first issue"
16
- pr-message: "Message that will be displayed on users' first pull request"
@@ -1,30 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
- permissions:
12
- contents: read
13
- id-token: write # only needed if you use --provenance
14
-
15
- steps:
16
- - uses: actions/checkout@v4
17
-
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: 20
21
- registry-url: "https://registry.npmjs.org"
22
-
23
- - run: npm install
24
-
25
- - run: npm run build --if-present
26
-
27
- - name: Publish
28
- run: npm publish --access public --provenance
29
- env:
30
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.gitmodules DELETED
@@ -1,4 +0,0 @@
1
- [submodule "src/wyze-api"]
2
- path = src/wyze-api
3
- url = https://github.com/jfarmer08/wyze-api
4
- branch = main
package/eslint.config.js DELETED
@@ -1,34 +0,0 @@
1
- const eslint = require('@eslint/js')
2
-
3
- module.exports = [
4
- {
5
- ignores: ['dist/**'],
6
- },
7
- {
8
- files: ['src/**/*.js'],
9
- ...eslint.configs.recommended,
10
- languageOptions: {
11
- ecmaVersion: 2022,
12
- sourceType: 'commonjs',
13
- globals: {
14
- module: 'readonly',
15
- require: 'readonly',
16
- __dirname: 'readonly',
17
- process: 'readonly',
18
- Buffer: 'readonly',
19
- setTimeout: 'readonly',
20
- clearTimeout: 'readonly',
21
- },
22
- },
23
- rules: {
24
- ...eslint.configs.recommended.rules,
25
- 'no-console': 'off',
26
- 'no-empty': ['error', { allowEmptyCatch: true }],
27
- curly: 'off',
28
- 'brace-style': 'off',
29
- eqeqeq: 'off',
30
- 'max-len': 'off',
31
- 'no-fallthrough': 'off',
32
- },
33
- },
34
- ]