iobroker.hassemu 1.2.0 → 1.3.0

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
@@ -36,16 +36,16 @@ ioBroker adapter that emulates a [Home Assistant](https://www.home-assistant.io)
36
36
  - **Node.js >= 20**
37
37
  - **ioBroker js-controller >= 7.0.7**
38
38
  - **ioBroker Admin >= 7.7.22**
39
- - **ioBroker web >= 8.0.0** — required for the VIS/Admin URL discovery that fills the mode-dropdown
39
+ - **ioBroker web >= 8.0.0**
40
40
 
41
41
  ---
42
42
 
43
43
  ## Ports
44
44
 
45
- | Port | Protocol | Purpose | Configurable |
46
- | ---- | -------- | -------------------------------------------- | ------------ |
47
- | 8123 | TCP/HTTP | Home Assistant emulation (HA standard port) | No — fixed |
48
- | 5353 | UDP | mDNS service broadcast (only if mDNS enabled)| No |
45
+ | Port | Protocol | Purpose | Configurable |
46
+ | ---- | -------- | --------------------------------------------- | ------------ |
47
+ | 8123 | TCP/HTTP | Home Assistant emulation (HA standard port) | No — fixed |
48
+ | 5353 | UDP | mDNS service broadcast (only if mDNS enabled) | No |
49
49
 
50
50
  ---
51
51
 
@@ -53,15 +53,13 @@ ioBroker adapter that emulates a [Home Assistant](https://www.home-assistant.io)
53
53
 
54
54
  The Admin UI configures the server. Redirect URLs are set via the state tree (see below).
55
55
 
56
- | Option | Description | Default |
57
- |--------|-------------|---------|
58
- | **Bind to Interface** | Network interface to listen on | 0.0.0.0 (all) |
59
- | **Service Name** | Name broadcast via mDNS, shown as the server name on the display | `ioBroker` |
60
- | **mDNS Enabled** | Broadcast `_home-assistant._tcp` on the LAN | `true` |
61
- | **Auth Required** | Check credentials the display sends during login | `false` |
62
- | **Username / Password** | Used when *Auth Required* is on (password encrypted at rest) | `admin` / — |
63
-
64
- > URLs you set must be reachable from the display — use the LAN IP of the ioBroker host, not `localhost`.
56
+ | Option | Description | Default |
57
+ | ----------------------- | ---------------------------------------------------------------- | ------------- |
58
+ | **Bind to Interface** | Network interface to listen on | 0.0.0.0 (all) |
59
+ | **Service Name** | Name broadcast via mDNS, shown as the server name on the display | `ioBroker` |
60
+ | **mDNS Enabled** | Broadcast `_home-assistant._tcp` on the LAN | `true` |
61
+ | **Auth Required** | Check credentials the display sends during login | `false` |
62
+ | **Username / Password** | Used when _Auth Required_ is on (password encrypted at rest) | `admin` / — |
65
63
 
66
64
  ---
67
65
 
@@ -86,11 +84,11 @@ hassemu.0.
86
84
 
87
85
  The adapter reads `clients.<id>.mode` on every visit:
88
86
 
89
- | `mode` value | redirect target |
90
- |--------------|----------------|
91
- | `global` | `global.mode` / `global.manualUrl` (same rules, one level up) |
92
- | `manual` | `clients.<id>.manualUrl` |
93
- | a URL | that URL |
87
+ | `mode` value | redirect target |
88
+ | --------------- | -------------------------------------------------------------- |
89
+ | `global` | `global.mode` / `global.manualUrl` (same rules, one level up) |
90
+ | `manual` | `clients.<id>.manualUrl` |
91
+ | a URL | that URL |
94
92
  | empty / unknown | landing page (small HTML with device ID, refreshes every 15 s) |
95
93
 
96
94
  ### Master switch
@@ -107,12 +105,12 @@ The adapter broadcasts `_home-assistant._tcp` via mDNS. If the display does not
107
105
 
108
106
  1. Check the adapter log for `mDNS: Broadcasting`.
109
107
  2. Verify the service is visible from another host:
110
- ```bash
111
- # macOS
112
- dns-sd -B _home-assistant._tcp
113
- # Linux (with avahi-utils)
114
- avahi-browse _home-assistant._tcp -r -t
115
- ```
108
+ ```bash
109
+ # macOS
110
+ dns-sd -B _home-assistant._tcp
111
+ # Linux (with avahi-utils)
112
+ avahi-browse _home-assistant._tcp -r -t
113
+ ```
116
114
  3. Make sure UDP 5353 is not blocked by a firewall.
117
115
  4. If mDNS is not usable on your LAN, set the URL manually on the display: `http://<ioBroker-IP>:8123`.
118
116
 
@@ -149,23 +147,33 @@ Reverse DNS on a home LAN depends on your router/DHCP server and often fails. Th
149
147
  ---
150
148
 
151
149
  ## Changelog
150
+ ### 1.3.0 (2026-04-30)
151
+
152
+ - Security: brute-force lockout on `/auth/login_flow/:flowId` — after 5 failed credential attempts an IP is rejected with HTTP 429 for 15 min. Successful login resets the counter.
153
+ - DRY refactor: shared `parseManualUrlWrite` helper between client + global config; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout window/threshold are now named constants instead of magic numbers.
154
+ - Dead-code cleanup: `resolveBindToReachable`, `coerceUuid` strict-V4 parameter, `DEFAULT_REFRESH_DEBOUNCE_MS` export, internal `getMode`/`getManualUrl` test affordances — all removed; tests rewritten to assert observable behaviour.
155
+ - New `landing-page` test suite with XSS-escape coverage and 11-language fallback verification.
156
+ - Emulated Home Assistant version bumped from 2026.3.1 to 2026.4.0.
157
+
152
158
  ### 1.2.0 (2026-04-29)
153
159
 
154
- - (krobi) Redirect target now configured via `mode` (dropdown) + `manualUrl` (free text) instead of the old `visUrl`. Migration runs automatically.
155
- - (krobi) Master switch `global.enabled` bulk-syncs every display: on → all follow the global URL, off → each display picks up its own again.
156
- - (krobi) Idle displays without auth token are auto-removed after 30 days.
157
- - (krobi) Security hardening of the auth flow.
158
- - (krobi) `web` adapter declared as dependency — needed for the URL dropdown.
160
+ - Redirect target now configured via `mode` (dropdown) + `manualUrl` (free text) instead of the old `visUrl`. Migration runs automatically.
161
+ - Master switch `global.enabled` syncs every display: on → all follow the global URL, off → each display picks up its own again.
162
+ - Idle displays without auth token are auto-removed after 30 days.
163
+ - Security hardening of the auth flow.
164
+ - `web` adapter declared as dependency.
159
165
 
160
166
  ### 1.1.6 (2026-04-28)
167
+
161
168
  - Audit cleanup against the upstream `ioBroker.example/TypeScript` full standard:
162
- - Test setup migrated: tests now live next to source as `src/lib/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`
163
- - `@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: ">=20"`
164
- - Dependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`
165
- - `nyc` config + `coverage` script added
166
- - Orphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)
169
+ - Test setup migrated: tests now live next to source as `src/lib/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`
170
+ - `@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: ">=20"`
171
+ - Dependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`
172
+ - `nyc` config + `coverage` script added
173
+ - Orphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)
167
174
 
168
175
  ### 1.1.5 (2026-04-26)
176
+
169
177
  - Process-level `unhandledRejection` / `uncaughtException` handlers added as last-line-of-defence against fire-and-forget rejections.
170
178
  - Stop shipping the `manual-review` release-script plugin — adapter-only consequence.
171
179
  - Audit-driven boilerplate sync with the other krobi adapters (`.vscode` json5 schemas, `tsconfig.test` looser test rules).
@@ -173,13 +181,10 @@ Reverse DNS on a home LAN depends on your router/DHCP server and often fails. Th
173
181
  - `@types/iobroker` bumped to `^7.1.1`.
174
182
 
175
183
  ### 1.1.4 (2026-04-23)
176
- - Separate test-build output (`build-test/`) from production `build/` — `npm test` no longer risks leaving duplicated `build/src` + `build/test` trees in the published package. No runtime change.
177
184
 
178
- ### 1.1.3 (2026-04-19)
185
+ - Separate test-build output (`build-test/`) from production `build/` — `npm test` no longer risks leaving duplicated `build/src` + `build/test` trees in the published package. No runtime change.
179
186
 
180
- - **Fix duplicate client registration on first connect** — HA displays fire several parallel cookieless requests (`GET /`, `GET /api/`, `POST /auth/login_flow`) within milliseconds of each other. Each used to create a separate client record, leaving orphans behind. The registry now locks per IP while the first client is being created, so parallel burst requests from the same display attach to the same client and cookie.
181
- - **Setup page redesigned** — big green OK banner so "everything's connected" is visible at a glance, responsive layout with dark-mode support, IP shown alongside the device ID, clearer step-by-step instructions.
182
- - **Setup page localized into all 11 adapter languages** — automatically picks the ioBroker system language (set in Admin → Main Settings), falls back to English for unknown languages.
187
+ Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
183
188
 
184
189
  ## Support
185
190
 
@@ -1,161 +1,161 @@
1
1
  {
2
- "i18n": true,
3
- "type": "panel",
4
- "items": {
5
- "_headerServer": {
6
- "type": "header",
7
- "text": "header_server",
8
- "size": 4
9
- },
10
- "bindAddress": {
11
- "type": "ip",
12
- "label": "bindAddress",
13
- "tooltip": "bindAddressTooltip",
14
- "listenOnAllPorts": true,
15
- "onlyIp4": true,
16
- "noInternal": false,
17
- "xs": 12,
18
- "sm": 6,
19
- "md": 4,
20
- "lg": 4,
21
- "xl": 4
22
- },
23
- "serviceName": {
24
- "type": "text",
25
- "label": "serviceName",
26
- "tooltip": "serviceNameTooltip",
27
- "default": "ioBroker",
28
- "xs": 12,
29
- "sm": 6,
30
- "md": 4,
31
- "lg": 4,
32
- "xl": 4
33
- },
34
- "_redirectInfo": {
35
- "type": "staticText",
36
- "text": "redirectInfo",
37
- "xs": 12,
38
- "sm": 12,
39
- "md": 12,
40
- "lg": 12,
41
- "xl": 12
42
- },
43
- "_headerMdns": {
44
- "type": "header",
45
- "text": "header_mdns",
46
- "size": 4
47
- },
48
- "mdnsEnabled": {
49
- "type": "checkbox",
50
- "label": "mdnsEnabled",
51
- "tooltip": "mdnsEnabledTooltip",
52
- "default": true,
53
- "xs": 12,
54
- "sm": 12,
55
- "md": 12,
56
- "lg": 12,
57
- "xl": 12
58
- },
59
- "_mdnsInfo": {
60
- "type": "staticText",
61
- "text": "mdnsInfo",
62
- "xs": 12,
63
- "sm": 12,
64
- "md": 12,
65
- "lg": 12,
66
- "xl": 12
67
- },
68
- "_headerAuth": {
69
- "type": "header",
70
- "text": "header_auth",
71
- "size": 4
72
- },
73
- "authRequired": {
74
- "type": "checkbox",
75
- "label": "authRequired",
76
- "tooltip": "authRequiredTooltip",
77
- "default": false,
78
- "xs": 12,
79
- "sm": 12,
80
- "md": 12,
81
- "lg": 12,
82
- "xl": 12
83
- },
84
- "username": {
85
- "type": "text",
86
- "label": "username",
87
- "default": "admin",
88
- "hidden": "!data.authRequired",
89
- "xs": 12,
90
- "sm": 6,
91
- "md": 6,
92
- "lg": 6,
93
- "xl": 6
94
- },
95
- "password": {
96
- "type": "password",
97
- "label": "password",
98
- "visible": true,
99
- "hidden": "!data.authRequired",
100
- "xs": 12,
101
- "sm": 6,
102
- "md": 6,
103
- "lg": 6,
104
- "xl": 6
105
- },
106
- "_authInfo": {
107
- "type": "staticText",
108
- "text": "authInfo",
109
- "hidden": "data.authRequired",
110
- "xs": 12,
111
- "sm": 12,
112
- "md": 12,
113
- "lg": 12,
114
- "xl": 12
115
- },
116
- "_supportHeader": {
117
- "type": "header",
118
- "text": "supportHeader",
119
- "size": 5
120
- },
121
- "_aboutInfo": {
122
- "type": "staticText",
123
- "text": "aboutInfo",
124
- "xs": 12,
125
- "sm": 12,
126
- "md": 12,
127
- "lg": 12,
128
- "xl": 12,
129
- "style": {
130
- "fontSize": 14,
131
- "marginBottom": 16
132
- }
133
- },
134
- "_kofiLink": {
135
- "type": "staticLink",
136
- "href": "https://ko-fi.com/krobipd",
137
- "label": "donateKofi",
138
- "button": true,
139
- "variant": "outlined",
140
- "color": "primary",
141
- "xs": 12,
142
- "sm": 6,
143
- "md": 4,
144
- "lg": 4,
145
- "xl": 4
146
- },
147
- "_paypalLink": {
148
- "type": "staticLink",
149
- "href": "https://paypal.me/krobipd",
150
- "label": "donatePaypal",
151
- "button": true,
152
- "variant": "outlined",
153
- "color": "primary",
154
- "xs": 12,
155
- "sm": 6,
156
- "md": 4,
157
- "lg": 4,
158
- "xl": 4
159
- }
160
- }
2
+ "i18n": true,
3
+ "type": "panel",
4
+ "items": {
5
+ "_headerServer": {
6
+ "type": "header",
7
+ "text": "header_server",
8
+ "size": 4
9
+ },
10
+ "bindAddress": {
11
+ "type": "ip",
12
+ "label": "bindAddress",
13
+ "tooltip": "bindAddressTooltip",
14
+ "listenOnAllPorts": true,
15
+ "onlyIp4": true,
16
+ "noInternal": false,
17
+ "xs": 12,
18
+ "sm": 6,
19
+ "md": 4,
20
+ "lg": 4,
21
+ "xl": 4
22
+ },
23
+ "serviceName": {
24
+ "type": "text",
25
+ "label": "serviceName",
26
+ "tooltip": "serviceNameTooltip",
27
+ "default": "ioBroker",
28
+ "xs": 12,
29
+ "sm": 6,
30
+ "md": 4,
31
+ "lg": 4,
32
+ "xl": 4
33
+ },
34
+ "_redirectInfo": {
35
+ "type": "staticText",
36
+ "text": "redirectInfo",
37
+ "xs": 12,
38
+ "sm": 12,
39
+ "md": 12,
40
+ "lg": 12,
41
+ "xl": 12
42
+ },
43
+ "_headerMdns": {
44
+ "type": "header",
45
+ "text": "header_mdns",
46
+ "size": 4
47
+ },
48
+ "mdnsEnabled": {
49
+ "type": "checkbox",
50
+ "label": "mdnsEnabled",
51
+ "tooltip": "mdnsEnabledTooltip",
52
+ "default": true,
53
+ "xs": 12,
54
+ "sm": 12,
55
+ "md": 12,
56
+ "lg": 12,
57
+ "xl": 12
58
+ },
59
+ "_mdnsInfo": {
60
+ "type": "staticText",
61
+ "text": "mdnsInfo",
62
+ "xs": 12,
63
+ "sm": 12,
64
+ "md": 12,
65
+ "lg": 12,
66
+ "xl": 12
67
+ },
68
+ "_headerAuth": {
69
+ "type": "header",
70
+ "text": "header_auth",
71
+ "size": 4
72
+ },
73
+ "authRequired": {
74
+ "type": "checkbox",
75
+ "label": "authRequired",
76
+ "tooltip": "authRequiredTooltip",
77
+ "default": false,
78
+ "xs": 12,
79
+ "sm": 12,
80
+ "md": 12,
81
+ "lg": 12,
82
+ "xl": 12
83
+ },
84
+ "username": {
85
+ "type": "text",
86
+ "label": "username",
87
+ "default": "admin",
88
+ "hidden": "!data.authRequired",
89
+ "xs": 12,
90
+ "sm": 6,
91
+ "md": 6,
92
+ "lg": 6,
93
+ "xl": 6
94
+ },
95
+ "password": {
96
+ "type": "password",
97
+ "label": "password",
98
+ "visible": true,
99
+ "hidden": "!data.authRequired",
100
+ "xs": 12,
101
+ "sm": 6,
102
+ "md": 6,
103
+ "lg": 6,
104
+ "xl": 6
105
+ },
106
+ "_authInfo": {
107
+ "type": "staticText",
108
+ "text": "authInfo",
109
+ "hidden": "data.authRequired",
110
+ "xs": 12,
111
+ "sm": 12,
112
+ "md": 12,
113
+ "lg": 12,
114
+ "xl": 12
115
+ },
116
+ "_supportHeader": {
117
+ "type": "header",
118
+ "text": "supportHeader",
119
+ "size": 5
120
+ },
121
+ "_aboutInfo": {
122
+ "type": "staticText",
123
+ "text": "aboutInfo",
124
+ "xs": 12,
125
+ "sm": 12,
126
+ "md": 12,
127
+ "lg": 12,
128
+ "xl": 12,
129
+ "style": {
130
+ "fontSize": 14,
131
+ "marginBottom": 16
132
+ }
133
+ },
134
+ "_kofiLink": {
135
+ "type": "staticLink",
136
+ "href": "https://ko-fi.com/krobipd",
137
+ "label": "donateKofi",
138
+ "button": true,
139
+ "variant": "outlined",
140
+ "color": "primary",
141
+ "xs": 12,
142
+ "sm": 6,
143
+ "md": 4,
144
+ "lg": 4,
145
+ "xl": 4
146
+ },
147
+ "_paypalLink": {
148
+ "type": "staticLink",
149
+ "href": "https://paypal.me/krobipd",
150
+ "label": "donatePaypal",
151
+ "button": true,
152
+ "variant": "outlined",
153
+ "color": "primary",
154
+ "xs": 12,
155
+ "sm": 6,
156
+ "md": 4,
157
+ "lg": 4,
158
+ "xl": 4
159
+ }
160
+ }
161
161
  }
@@ -251,21 +251,20 @@ class ClientRegistry {
251
251
  * @param rawValue Value written to the state.
252
252
  */
253
253
  async handleManualUrlWrite(id, rawValue) {
254
- var _a;
254
+ var _a, _b;
255
255
  const record = this.byId.get(id);
256
256
  if (!record) {
257
257
  return;
258
258
  }
259
- const empty = rawValue === "" || rawValue === null || rawValue === void 0;
260
- const safe = empty ? null : (0, import_coerce.coerceSafeUrl)(rawValue);
261
- if (!empty && !safe) {
259
+ const result = (0, import_coerce.parseManualUrlWrite)(rawValue);
260
+ if (!result.ok) {
262
261
  this.adapter.log.warn(`client-registry: rejected unsafe manualUrl for ${id}`);
263
262
  await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: (_a = record.manualUrl) != null ? _a : "", ack: true });
264
263
  return;
265
264
  }
266
- record.manualUrl = safe;
267
- await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: safe != null ? safe : "", ack: true });
268
- if (record.mode === import_global_config.MODE_MANUAL && !safe) {
265
+ record.manualUrl = result.safe;
266
+ await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: (_b = result.safe) != null ? _b : "", ack: true });
267
+ if (record.mode === import_global_config.MODE_MANUAL && !result.safe) {
269
268
  this.adapter.log.warn(
270
269
  `client-registry: ${id} manualUrl cleared while mode='manual' \u2014 display will hit the setup page`
271
270
  );