iobroker.hassemu 1.35.3 → 1.36.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
@@ -55,7 +55,7 @@ Want to add a URL the adapter doesn't auto-detect? Set `manual` and paste it.
55
55
  ## Requirements
56
56
 
57
57
  - Node.js ≥ 22
58
- - ioBroker js-controller ≥ 7.1.2
58
+ - ioBroker js-controller ≥ 7.2.2
59
59
  - ioBroker Admin ≥ 7.8.23
60
60
 
61
61
  ---
@@ -160,6 +160,12 @@ Got scripts that still write to `visUrl`? Update them — write to `manualUrl` i
160
160
  Placeholder for the next version (at the beginning of the line):
161
161
  ### **WORK IN PROGRESS**
162
162
  -->
163
+ ### 1.36.0 (2026-06-22)
164
+ - Fixed a rare adapter crash and restart loop that a malformed connection message could trigger — it briefly took all connected displays offline until the adapter recovered.
165
+ - A custom name you give a display (its channel name) is no longer overwritten with the device's IP address when that IP changes.
166
+ - With authentication enabled, a display again reloads automatically after you change its target URL.
167
+ - With authentication enabled, a password is now required — the settings can no longer be saved with an empty password.
168
+
163
169
  ### 1.35.3 (2026-06-15)
164
170
  - Fixed Home Assistant discovery pointing the display at the wrong address on multi-interface hosts; it now uses the address the adapter actually listens on.
165
171
 
@@ -176,10 +182,6 @@ Got scripts that still write to `visUrl`? Update them — write to `manualUrl` i
176
182
 
177
183
  - Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.
178
184
 
179
- ### 1.34.0 (2026-06-02)
180
-
181
- - Home Assistant Companion App and Shelly Wall Display (firmware 2.6.0+): sign-out and device registration now complete reliably.
182
-
183
185
  [Older changelogs can be found there](CHANGELOG_OLD.md)
184
186
 
185
187
  ## Support Development
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "Die Seite oben ist die letzte Ansicht, die das Display erhalten hat. Sobald hassemu wieder erreichbar ist, aktualisiert sich die Anzeige automatisch.",
56
56
  "pageReload": "Jetzt neu laden",
57
57
  "pageDeviceId": "Geräte-ID",
58
- "pageIpAddress": "IP-Adresse"
58
+ "pageIpAddress": "IP-Adresse",
59
+ "passwordRequiredWhenAuth": "Bei aktivierter Authentifizierung ist ein Passwort erforderlich."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "The page above is the last view this display received. As soon as hassemu is reachable again, the display updates by itself.",
56
56
  "pageReload": "Reload now",
57
57
  "pageDeviceId": "Device ID",
58
- "pageIpAddress": "IP address"
58
+ "pageIpAddress": "IP address",
59
+ "passwordRequiredWhenAuth": "A password is required when authentication is enabled."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "La página de arriba es la última vista que recibió esta pantalla. En cuanto hassemu vuelva a estar disponible, la pantalla se actualizará automáticamente.",
56
56
  "pageReload": "Recargar",
57
57
  "pageDeviceId": "ID del dispositivo",
58
- "pageIpAddress": "Dirección IP"
58
+ "pageIpAddress": "Dirección IP",
59
+ "passwordRequiredWhenAuth": "Se requiere una contraseña cuando la autenticación está habilitada."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "La page ci-dessus est la dernière vue reçue par cet écran. Dès que hassemu est de nouveau accessible, l'écran se met à jour automatiquement.",
56
56
  "pageReload": "Recharger",
57
57
  "pageDeviceId": "Identifiant de l'appareil",
58
- "pageIpAddress": "Adresse IP"
58
+ "pageIpAddress": "Adresse IP",
59
+ "passwordRequiredWhenAuth": "Un mot de passe est requis lorsque l'authentification est activée."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "La pagina sopra è l’ultima vista ricevuta dal display. Appena hassemu sarà nuovamente raggiungibile, la schermata si aggiornerà automaticamente.",
56
56
  "pageReload": "Ricarica",
57
57
  "pageDeviceId": "ID dispositivo",
58
- "pageIpAddress": "Indirizzo IP"
58
+ "pageIpAddress": "Indirizzo IP",
59
+ "passwordRequiredWhenAuth": "È richiesta una password quando l'autenticazione è abilitata."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "De pagina hierboven is de laatste weergave die dit display heeft ontvangen. Zodra hassemu weer bereikbaar is, wordt het scherm automatisch bijgewerkt.",
56
56
  "pageReload": "Opnieuw laden",
57
57
  "pageDeviceId": "Apparaat-ID",
58
- "pageIpAddress": "IP-adres"
58
+ "pageIpAddress": "IP-adres",
59
+ "passwordRequiredWhenAuth": "Een wachtwoord is vereist wanneer authenticatie is ingeschakeld."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "Strona powyżej to ostatni widok otrzymany przez ten wyświetlacz. Gdy hassemu znów będzie dostępny, ekran odświeży się sam.",
56
56
  "pageReload": "Załaduj ponownie",
57
57
  "pageDeviceId": "ID urządzenia",
58
- "pageIpAddress": "Adres IP"
58
+ "pageIpAddress": "Adres IP",
59
+ "passwordRequiredWhenAuth": "Hasło jest wymagane, gdy uwierzytelnianie jest włączone."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "A página acima é a última visualização que este ecrã recebeu. Assim que o hassemu voltar a estar disponível, o ecrã atualiza-se automaticamente.",
56
56
  "pageReload": "Recarregar",
57
57
  "pageDeviceId": "ID do dispositivo",
58
- "pageIpAddress": "Endereço IP"
58
+ "pageIpAddress": "Endereço IP",
59
+ "passwordRequiredWhenAuth": "É necessária uma palavra-passe quando a autenticação está ativada."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "Страница выше — последний вид, который получил дисплей. Как только hassemu снова доступен, экран обновится автоматически.",
56
56
  "pageReload": "Перезагрузить",
57
57
  "pageDeviceId": "ID устройства",
58
- "pageIpAddress": "IP-адрес"
58
+ "pageIpAddress": "IP-адрес",
59
+ "passwordRequiredWhenAuth": "При включённой аутентификации необходимо указать пароль."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "Сторінка вище — останній вигляд, який отримав цей дисплей. Щойно hassemu знову буде доступним, екран оновиться автоматично.",
56
56
  "pageReload": "Перезавантажити",
57
57
  "pageDeviceId": "ID пристрою",
58
- "pageIpAddress": "IP-адреса"
58
+ "pageIpAddress": "IP-адреса",
59
+ "passwordRequiredWhenAuth": "Коли автентифікацію ввімкнено, потрібен пароль."
59
60
  }
@@ -55,5 +55,6 @@
55
55
  "pageOfflineSubhead": "上方页面是此显示器收到的最后一次视图。hassemu 重新可用后,屏幕会自动刷新。",
56
56
  "pageReload": "立即重新加载",
57
57
  "pageDeviceId": "设备 ID",
58
- "pageIpAddress": "IP 地址"
58
+ "pageIpAddress": "IP 地址",
59
+ "passwordRequiredWhenAuth": "启用身份验证时需要设置密码。"
59
60
  }
@@ -97,6 +97,8 @@
97
97
  "label": "password",
98
98
  "visible": true,
99
99
  "hidden": "!data.authRequired",
100
+ "validator": "!data.authRequired || !!data.password",
101
+ "validatorErrorText": "passwordRequiredWhenAuth",
100
102
  "xs": 12,
101
103
  "sm": 6,
102
104
  "md": 6,
@@ -69,7 +69,7 @@ ${opts.bodyExtra}` : ""}
69
69
  }
70
70
  function renderAuthorizeRedirect(target) {
71
71
  const a = (0, import_coerce.escapeHtml)(target);
72
- const j = JSON.stringify(target);
72
+ const j = JSON.stringify(target).replace(/</g, "\\u003C");
73
73
  return htmlShell({
74
74
  title: "Home Assistant",
75
75
  headExtra: `<meta http-equiv="refresh" content="0; URL=${a}">`,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/auth-page.ts"],
4
- "sourcesContent": ["/**\n * Minimal HTML templates for the OAuth2 browser-flow at `/auth/authorize`.\n *\n * The real Home Assistant frontend renders the auth UI via a multi-MB\n * Lit/Polymer Web Component bundle. For hassemu we re-implement only the\n * subset the HA Companion App and the Shelly Wall Display app actually need:\n *\n * - `authRequired=false`: auto-submit page that calls `document.location.assign`\n * with `redirect_uri?code=\u2026&state=\u2026` immediately. Matches HA's frontend\n * redirect helper `redirectWithAuthCode` 1:1.\n * - `authRequired=true`: a minimal username/password form that POSTs to\n * `/auth/authorize`. The handler verifies credentials and replies with\n * the same auto-submit redirect on success, or re-renders the form with\n * an error banner on failure.\n * - `400` error page when query parameters are malformed or the\n * `redirect_uri` fails validation \u2014 never redirects, so an attacker\n * cannot use the endpoint as an open redirector.\n *\n * Source: `home-assistant/frontend/src/data/auth.ts:redirectWithAuthCode` \u2014\n * `document.location.assign(url)` with `code=<encoded>&state=<encoded>`.\n */\n\n// v1.32.0: lokales `escAttr` (4 Char) ersetzt durch shared `escapeHtml` (5 Char)\n// aus `coerce.ts` \u2014 defense-in-depth, `'` zus\u00E4tzlich geh\u00E4rtet.\nimport { escapeHtml as escAttr } from \"./coerce\";\n\n/**\n * Build the final redirect URL with the auth code appended.\n *\n * @param redirectUri Already-validated `redirect_uri` from the OAuth2 query.\n * @param code Auth code to deliver (will be URL-encoded).\n * @param state Optional `state` parameter \u2014 round-tripped verbatim per OAuth2 CSRF.\n */\nexport function buildRedirectUrl(redirectUri: string, code: string, state: string | undefined): string {\n // Source: home-assistant/frontend/src/data/auth.ts \u2014 OAuth 2: 3.1.2 we\n // need to retain the existing query of a redirect URI.\n let url = redirectUri;\n if (!url.includes(\"?\")) {\n url += \"?\";\n } else if (!url.endsWith(\"&\") && !url.endsWith(\"?\")) {\n url += \"&\";\n }\n url += `code=${encodeURIComponent(code)}`;\n if (state) {\n url += `&state=${encodeURIComponent(state)}`;\n }\n return url;\n}\n\nconst STYLE = `\nhtml,body{margin:0;padding:0;height:100%;background:#111;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;}\n.card{max-width:380px;margin:64px auto;padding:32px;background:#1c1c1c;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.5);}\nh1{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center;color:#03a9f4;}\n.subtitle{color:#888;font-size:14px;text-align:center;margin:-16px 0 24px;}\n.err{background:#3a1a1a;color:#ff8a80;padding:12px;border-radius:4px;margin-bottom:16px;font-size:14px;}\ninput{display:block;width:100%;padding:12px;margin:8px 0 16px;background:#2a2a2a;border:1px solid #444;color:#fff;border-radius:4px;font-size:16px;box-sizing:border-box;}\ninput:focus{outline:none;border-color:#03a9f4;}\nbutton{display:block;width:100%;padding:14px;background:#03a9f4;color:#fff;border:none;border-radius:4px;font-size:16px;cursor:pointer;}\nbutton:hover{background:#039be5;}\n.loading{text-align:center;color:#888;padding:16px;}\n`.trim();\n\n/**\n * Shared page frame for the 3 auth pages \u2014 identical DOCTYPE / `<head>` / style /\n * `.card` wrapper. Each page supplies its own `<title>`, an extra `<head>` line\n * (viewport or the redirect `meta refresh`), the card body, and optional trailing\n * body markup (the redirect `<script>`).\n *\n * @param opts Page parts to assemble into the shared shell.\n * @param opts.title `<title>` text (already escaped by the caller if dynamic).\n * @param opts.headExtra One extra `<head>` line (viewport meta / refresh meta).\n * @param opts.cardInner Inner HTML of the `.card` container.\n * @param opts.bodyExtra Optional markup appended after the card (e.g. the redirect script).\n */\nfunction htmlShell(opts: { title: string; headExtra: string; cardInner: string; bodyExtra?: string }): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n${opts.headExtra}\n<title>${opts.title}</title>\n<style>${STYLE}</style>\n</head>\n<body>\n<div class=\"card\">\n${opts.cardInner}\n</div>${opts.bodyExtra ? `\\n${opts.bodyExtra}` : \"\"}\n</body>\n</html>`;\n}\n\n/**\n * Render the auto-submit redirect page. Used when `authRequired=false` (after\n * generating an auth_code) OR after a successful POST login.\n *\n * Uses `document.location.assign(url)` to match HA's `redirectWithAuthCode`.\n * `<meta http-equiv=\"refresh\">` as a fallback for clients without JS, though\n * the HA Companion App and Shelly Wall Display WebView both have JS enabled.\n *\n * @param target The fully-built `redirect_uri?code=\u2026&state=\u2026` URL.\n */\nexport function renderAuthorizeRedirect(target: string): string {\n const a = escAttr(target);\n const j = JSON.stringify(target); // safe for inline JS string literal\n return htmlShell({\n title: \"Home Assistant\",\n headExtra: `<meta http-equiv=\"refresh\" content=\"0; URL=${a}\">`,\n cardInner: `<h1>Home Assistant</h1>\n<p class=\"loading\">Signing in\u2026</p>`,\n bodyExtra: `<script>(function(){document.location.assign(${j});})();</script>`,\n });\n}\n\n/**\n * Render the login form. Used on GET `/auth/authorize` when `authRequired=true`,\n * and re-used by the POST handler on credential failure with the error banner.\n *\n * Hidden fields preserve the OAuth2 query params across the form-submit so the\n * POST handler can finish the flow.\n *\n * @param params Form context \u2014 `client_id`, `redirect_uri`, `state` (optional).\n * @param params.clientId The OAuth2 `client_id` to round-trip in a hidden input.\n * @param params.redirectUri Already-validated `redirect_uri` to round-trip in a hidden input.\n * @param params.state Optional OAuth2 `state` (CSRF token) to round-trip.\n * @param errorMessage Optional error banner text (i.e. \"Invalid username or password.\").\n */\nexport function renderAuthorizeForm(\n params: { clientId: string; redirectUri: string; state?: string },\n errorMessage?: string,\n): string {\n const cid = escAttr(params.clientId);\n const ru = escAttr(params.redirectUri);\n const st = params.state ? escAttr(params.state) : \"\";\n const errBlock = errorMessage ? `<div class=\"err\">${escAttr(errorMessage)}</div>` : \"\";\n return htmlShell({\n title: \"Home Assistant \u2014 Sign In\",\n headExtra: `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">`,\n cardInner: `<h1>Home Assistant</h1>\n<p class=\"subtitle\">Sign in to authorize this device.</p>\n${errBlock}\n<form method=\"POST\" action=\"/auth/authorize\" autocomplete=\"off\">\n<input type=\"hidden\" name=\"response_type\" value=\"code\">\n<input type=\"hidden\" name=\"client_id\" value=\"${cid}\">\n<input type=\"hidden\" name=\"redirect_uri\" value=\"${ru}\">\n<input type=\"hidden\" name=\"state\" value=\"${st}\">\n<input type=\"text\" name=\"username\" placeholder=\"Username\" autofocus required>\n<input type=\"password\" name=\"password\" placeholder=\"Password\" required>\n<button type=\"submit\">Sign in</button>\n</form>`,\n });\n}\n\n/**\n * Render the authorization error page. Used when query params are malformed\n * or when `redirect_uri` fails validation. NEVER auto-redirects \u2014 we don't\n * want to leak codes to attacker-controlled URIs by accident.\n *\n * @param reason Short OAuth2 error code (e.g. `invalid_request`).\n * @param detail Human-readable explanation.\n */\nexport function renderAuthorizeError(reason: string, detail: string): string {\n return htmlShell({\n title: `Home Assistant \u2014 ${escAttr(reason)}`,\n headExtra: `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">`,\n cardInner: `<h1>Authorization failed</h1>\n<div class=\"err\">${escAttr(detail)}</div>\n<p class=\"subtitle\">${escAttr(reason)}</p>`,\n });\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBA,oBAAsC;AAS/B,SAAS,iBAAiB,aAAqB,MAAc,OAAmC;AAGrG,MAAI,MAAM;AACV,MAAI,CAAC,IAAI,SAAS,GAAG,GAAG;AACtB,WAAO;AAAA,EACT,WAAW,CAAC,IAAI,SAAS,GAAG,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG;AACnD,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,mBAAmB,IAAI,CAAC;AACvC,MAAI,OAAO;AACT,WAAO,UAAU,mBAAmB,KAAK,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;AAEA,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWZ,KAAK;AAcP,SAAS,UAAU,MAA2F;AAC5G,SAAO;AAAA;AAAA;AAAA;AAAA,EAIP,KAAK,SAAS;AAAA,SACP,KAAK,KAAK;AAAA,SACV,KAAK;AAAA;AAAA;AAAA;AAAA,EAIZ,KAAK,SAAS;AAAA,QACR,KAAK,YAAY;AAAA,EAAK,KAAK,SAAS,KAAK,EAAE;AAAA;AAAA;AAGnD;AAYO,SAAS,wBAAwB,QAAwB;AAC9D,QAAM,QAAI,cAAAA,YAAQ,MAAM;AACxB,QAAM,IAAI,KAAK,UAAU,MAAM;AAC/B,SAAO,UAAU;AAAA,IACf,OAAO;AAAA,IACP,WAAW,8CAA8C,CAAC;AAAA,IAC1D,WAAW;AAAA;AAAA,IAEX,WAAW,gDAAgD,CAAC;AAAA,EAC9D,CAAC;AACH;AAeO,SAAS,oBACd,QACA,cACQ;AACR,QAAM,UAAM,cAAAA,YAAQ,OAAO,QAAQ;AACnC,QAAM,SAAK,cAAAA,YAAQ,OAAO,WAAW;AACrC,QAAM,KAAK,OAAO,YAAQ,cAAAA,YAAQ,OAAO,KAAK,IAAI;AAClD,QAAM,WAAW,eAAe,wBAAoB,cAAAA,YAAQ,YAAY,CAAC,WAAW;AACpF,SAAO,UAAU;AAAA,IACf,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA;AAAA,EAEb,QAAQ;AAAA;AAAA;AAAA,+CAGqC,GAAG;AAAA,kDACA,EAAE;AAAA,2CACT,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,CAAC;AACH;AAUO,SAAS,qBAAqB,QAAgB,QAAwB;AAC3E,SAAO,UAAU;AAAA,IACf,OAAO,6BAAoB,cAAAA,YAAQ,MAAM,CAAC;AAAA,IAC1C,WAAW;AAAA,IACX,WAAW;AAAA,uBACI,cAAAA,YAAQ,MAAM,CAAC;AAAA,0BACZ,cAAAA,YAAQ,MAAM,CAAC;AAAA,EACnC,CAAC;AACH;",
4
+ "sourcesContent": ["/**\n * Minimal HTML templates for the OAuth2 browser-flow at `/auth/authorize`.\n *\n * The real Home Assistant frontend renders the auth UI via a multi-MB\n * Lit/Polymer Web Component bundle. For hassemu we re-implement only the\n * subset the HA Companion App and the Shelly Wall Display app actually need:\n *\n * - `authRequired=false`: auto-submit page that calls `document.location.assign`\n * with `redirect_uri?code=\u2026&state=\u2026` immediately. Matches HA's frontend\n * redirect helper `redirectWithAuthCode` 1:1.\n * - `authRequired=true`: a minimal username/password form that POSTs to\n * `/auth/authorize`. The handler verifies credentials and replies with\n * the same auto-submit redirect on success, or re-renders the form with\n * an error banner on failure.\n * - `400` error page when query parameters are malformed or the\n * `redirect_uri` fails validation \u2014 never redirects, so an attacker\n * cannot use the endpoint as an open redirector.\n *\n * Source: `home-assistant/frontend/src/data/auth.ts:redirectWithAuthCode` \u2014\n * `document.location.assign(url)` with `code=<encoded>&state=<encoded>`.\n */\n\n// v1.32.0: lokales `escAttr` (4 Char) ersetzt durch shared `escapeHtml` (5 Char)\n// aus `coerce.ts` \u2014 defense-in-depth, `'` zus\u00E4tzlich geh\u00E4rtet.\nimport { escapeHtml as escAttr } from \"./coerce\";\n\n/**\n * Build the final redirect URL with the auth code appended.\n *\n * @param redirectUri Already-validated `redirect_uri` from the OAuth2 query.\n * @param code Auth code to deliver (will be URL-encoded).\n * @param state Optional `state` parameter \u2014 round-tripped verbatim per OAuth2 CSRF.\n */\nexport function buildRedirectUrl(redirectUri: string, code: string, state: string | undefined): string {\n // Source: home-assistant/frontend/src/data/auth.ts \u2014 OAuth 2: 3.1.2 we\n // need to retain the existing query of a redirect URI.\n let url = redirectUri;\n if (!url.includes(\"?\")) {\n url += \"?\";\n } else if (!url.endsWith(\"&\") && !url.endsWith(\"?\")) {\n url += \"&\";\n }\n url += `code=${encodeURIComponent(code)}`;\n if (state) {\n url += `&state=${encodeURIComponent(state)}`;\n }\n return url;\n}\n\nconst STYLE = `\nhtml,body{margin:0;padding:0;height:100%;background:#111;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;}\n.card{max-width:380px;margin:64px auto;padding:32px;background:#1c1c1c;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.5);}\nh1{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center;color:#03a9f4;}\n.subtitle{color:#888;font-size:14px;text-align:center;margin:-16px 0 24px;}\n.err{background:#3a1a1a;color:#ff8a80;padding:12px;border-radius:4px;margin-bottom:16px;font-size:14px;}\ninput{display:block;width:100%;padding:12px;margin:8px 0 16px;background:#2a2a2a;border:1px solid #444;color:#fff;border-radius:4px;font-size:16px;box-sizing:border-box;}\ninput:focus{outline:none;border-color:#03a9f4;}\nbutton{display:block;width:100%;padding:14px;background:#03a9f4;color:#fff;border:none;border-radius:4px;font-size:16px;cursor:pointer;}\nbutton:hover{background:#039be5;}\n.loading{text-align:center;color:#888;padding:16px;}\n`.trim();\n\n/**\n * Shared page frame for the 3 auth pages \u2014 identical DOCTYPE / `<head>` / style /\n * `.card` wrapper. Each page supplies its own `<title>`, an extra `<head>` line\n * (viewport or the redirect `meta refresh`), the card body, and optional trailing\n * body markup (the redirect `<script>`).\n *\n * @param opts Page parts to assemble into the shared shell.\n * @param opts.title `<title>` text (already escaped by the caller if dynamic).\n * @param opts.headExtra One extra `<head>` line (viewport meta / refresh meta).\n * @param opts.cardInner Inner HTML of the `.card` container.\n * @param opts.bodyExtra Optional markup appended after the card (e.g. the redirect script).\n */\nfunction htmlShell(opts: { title: string; headExtra: string; cardInner: string; bodyExtra?: string }): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n${opts.headExtra}\n<title>${opts.title}</title>\n<style>${STYLE}</style>\n</head>\n<body>\n<div class=\"card\">\n${opts.cardInner}\n</div>${opts.bodyExtra ? `\\n${opts.bodyExtra}` : \"\"}\n</body>\n</html>`;\n}\n\n/**\n * Render the auto-submit redirect page. Used when `authRequired=false` (after\n * generating an auth_code) OR after a successful POST login.\n *\n * Uses `document.location.assign(url)` to match HA's `redirectWithAuthCode`.\n * `<meta http-equiv=\"refresh\">` as a fallback for clients without JS, though\n * the HA Companion App and Shelly Wall Display WebView both have JS enabled.\n *\n * @param target The fully-built `redirect_uri?code=\u2026&state=\u2026` URL.\n */\nexport function renderAuthorizeRedirect(target: string): string {\n const a = escAttr(target);\n // A `</script>` inside `target` would close the inline <script> at the HTML\n // tokenizer level (before JS parses the string literal), so JSON.stringify\n // alone is NOT enough \u2014 also escape `<` to its JS unicode escape `<`.\n const j = JSON.stringify(target).replace(/</g, \"\\\\u003C\");\n return htmlShell({\n title: \"Home Assistant\",\n headExtra: `<meta http-equiv=\"refresh\" content=\"0; URL=${a}\">`,\n cardInner: `<h1>Home Assistant</h1>\n<p class=\"loading\">Signing in\u2026</p>`,\n bodyExtra: `<script>(function(){document.location.assign(${j});})();</script>`,\n });\n}\n\n/**\n * Render the login form. Used on GET `/auth/authorize` when `authRequired=true`,\n * and re-used by the POST handler on credential failure with the error banner.\n *\n * Hidden fields preserve the OAuth2 query params across the form-submit so the\n * POST handler can finish the flow.\n *\n * @param params Form context \u2014 `client_id`, `redirect_uri`, `state` (optional).\n * @param params.clientId The OAuth2 `client_id` to round-trip in a hidden input.\n * @param params.redirectUri Already-validated `redirect_uri` to round-trip in a hidden input.\n * @param params.state Optional OAuth2 `state` (CSRF token) to round-trip.\n * @param errorMessage Optional error banner text (i.e. \"Invalid username or password.\").\n */\nexport function renderAuthorizeForm(\n params: { clientId: string; redirectUri: string; state?: string },\n errorMessage?: string,\n): string {\n const cid = escAttr(params.clientId);\n const ru = escAttr(params.redirectUri);\n const st = params.state ? escAttr(params.state) : \"\";\n const errBlock = errorMessage ? `<div class=\"err\">${escAttr(errorMessage)}</div>` : \"\";\n return htmlShell({\n title: \"Home Assistant \u2014 Sign In\",\n headExtra: `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">`,\n cardInner: `<h1>Home Assistant</h1>\n<p class=\"subtitle\">Sign in to authorize this device.</p>\n${errBlock}\n<form method=\"POST\" action=\"/auth/authorize\" autocomplete=\"off\">\n<input type=\"hidden\" name=\"response_type\" value=\"code\">\n<input type=\"hidden\" name=\"client_id\" value=\"${cid}\">\n<input type=\"hidden\" name=\"redirect_uri\" value=\"${ru}\">\n<input type=\"hidden\" name=\"state\" value=\"${st}\">\n<input type=\"text\" name=\"username\" placeholder=\"Username\" autofocus required>\n<input type=\"password\" name=\"password\" placeholder=\"Password\" required>\n<button type=\"submit\">Sign in</button>\n</form>`,\n });\n}\n\n/**\n * Render the authorization error page. Used when query params are malformed\n * or when `redirect_uri` fails validation. NEVER auto-redirects \u2014 we don't\n * want to leak codes to attacker-controlled URIs by accident.\n *\n * @param reason Short OAuth2 error code (e.g. `invalid_request`).\n * @param detail Human-readable explanation.\n */\nexport function renderAuthorizeError(reason: string, detail: string): string {\n return htmlShell({\n title: `Home Assistant \u2014 ${escAttr(reason)}`,\n headExtra: `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">`,\n cardInner: `<h1>Authorization failed</h1>\n<div class=\"err\">${escAttr(detail)}</div>\n<p class=\"subtitle\">${escAttr(reason)}</p>`,\n });\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBA,oBAAsC;AAS/B,SAAS,iBAAiB,aAAqB,MAAc,OAAmC;AAGrG,MAAI,MAAM;AACV,MAAI,CAAC,IAAI,SAAS,GAAG,GAAG;AACtB,WAAO;AAAA,EACT,WAAW,CAAC,IAAI,SAAS,GAAG,KAAK,CAAC,IAAI,SAAS,GAAG,GAAG;AACnD,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,mBAAmB,IAAI,CAAC;AACvC,MAAI,OAAO;AACT,WAAO,UAAU,mBAAmB,KAAK,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;AAEA,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWZ,KAAK;AAcP,SAAS,UAAU,MAA2F;AAC5G,SAAO;AAAA;AAAA;AAAA;AAAA,EAIP,KAAK,SAAS;AAAA,SACP,KAAK,KAAK;AAAA,SACV,KAAK;AAAA;AAAA;AAAA;AAAA,EAIZ,KAAK,SAAS;AAAA,QACR,KAAK,YAAY;AAAA,EAAK,KAAK,SAAS,KAAK,EAAE;AAAA;AAAA;AAGnD;AAYO,SAAS,wBAAwB,QAAwB;AAC9D,QAAM,QAAI,cAAAA,YAAQ,MAAM;AAIxB,QAAM,IAAI,KAAK,UAAU,MAAM,EAAE,QAAQ,MAAM,SAAS;AACxD,SAAO,UAAU;AAAA,IACf,OAAO;AAAA,IACP,WAAW,8CAA8C,CAAC;AAAA,IAC1D,WAAW;AAAA;AAAA,IAEX,WAAW,gDAAgD,CAAC;AAAA,EAC9D,CAAC;AACH;AAeO,SAAS,oBACd,QACA,cACQ;AACR,QAAM,UAAM,cAAAA,YAAQ,OAAO,QAAQ;AACnC,QAAM,SAAK,cAAAA,YAAQ,OAAO,WAAW;AACrC,QAAM,KAAK,OAAO,YAAQ,cAAAA,YAAQ,OAAO,KAAK,IAAI;AAClD,QAAM,WAAW,eAAe,wBAAoB,cAAAA,YAAQ,YAAY,CAAC,WAAW;AACpF,SAAO,UAAU;AAAA,IACf,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA;AAAA,EAEb,QAAQ;AAAA;AAAA;AAAA,+CAGqC,GAAG;AAAA,kDACA,EAAE;AAAA,2CACT,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,CAAC;AACH;AAUO,SAAS,qBAAqB,QAAgB,QAAwB;AAC3E,SAAO,UAAU;AAAA,IACf,OAAO,6BAAoB,cAAAA,YAAQ,MAAM,CAAC;AAAA,IAC1C,WAAW;AAAA,IACX,WAAW;AAAA,uBACI,cAAAA,YAAQ,MAAM,CAAC;AAAA,0BACZ,cAAAA,YAAQ,MAAM,CAAC;AAAA,EACnC,CAAC;AACH;",
6
6
  "names": ["escAttr"]
7
7
  }
@@ -112,6 +112,7 @@ class ClientRegistry {
112
112
  const ip = (0, import_coerce.coerceString)(ipRaw);
113
113
  const token = (0, import_coerce.coerceUuid)(native.token);
114
114
  const refreshToken = (0, import_coerce.coerceUuid)(native.refreshToken);
115
+ const tokenExpiresAt = token && typeof native.tokenExpiresAt === "number" ? native.tokenExpiresAt : null;
115
116
  const legacyHostname = (0, import_coerce.coerceString)(hostnameRaw);
116
117
  let channelName = (0, import_coerce.coerceString)((_b = obj.common) == null ? void 0 : _b.name);
117
118
  if (legacyHostname) {
@@ -128,7 +129,7 @@ class ClientRegistry {
128
129
  }
129
130
  }
130
131
  const hostname = channelName && channelName !== ip && channelName !== id ? channelName : null;
131
- const record = { id, cookie, token, refreshToken, mode, manualUrl, ip, hostname };
132
+ const record = { id, cookie, token, tokenExpiresAt, refreshToken, mode, manualUrl, ip, hostname };
132
133
  this.trackInMemory(record);
133
134
  await this.ensureObjects(record);
134
135
  const modeStateRaw = await this.readState(`${id}.mode`);
@@ -161,6 +162,10 @@ class ClientRegistry {
161
162
  }
162
163
  }
163
164
  if (ip) {
165
+ if (this.isIpThrottled(ip)) {
166
+ this.adapter.log.debug(`identify: IP ${ip} over new-client throttle \u2014 serving a transient record (no object)`);
167
+ return this.transientRecord(ip, hostname);
168
+ }
164
169
  const bucketKey = userAgent ? `${ip}|${import_node_crypto.default.createHash("sha256").update(userAgent).digest("hex").substring(0, 12)}` : ip;
165
170
  const pending = this.pendingByIp.get(bucketKey);
166
171
  if (pending) {
@@ -209,8 +214,18 @@ class ClientRegistry {
209
214
  * @param token Bearer token.
210
215
  */
211
216
  getByToken(token) {
212
- var _a;
213
- return (_a = this.byToken.get(token)) != null ? _a : null;
217
+ const record = this.byToken.get(token);
218
+ if (!record) {
219
+ return null;
220
+ }
221
+ if (record.tokenExpiresAt != null && Date.now() > record.tokenExpiresAt) {
222
+ this.byToken.delete(token);
223
+ if (record.token === token) {
224
+ record.token = null;
225
+ }
226
+ return null;
227
+ }
228
+ return record;
214
229
  }
215
230
  /**
216
231
  * Lookup by refresh token issued during the auth flow.
@@ -240,10 +255,13 @@ class ClientRegistry {
240
255
  this.byToken.delete(record.token);
241
256
  }
242
257
  record.token = token;
258
+ record.tokenExpiresAt = token ? Date.now() + import_constants.OAUTH_ACCESS_TOKEN_TTL_S * 1e3 : null;
243
259
  if (token) {
244
260
  this.byToken.set(token, record);
245
261
  }
246
- await this.adapter.extendObjectAsync(`clients.${id}`, { native: { token } });
262
+ await this.adapter.extendObjectAsync(`clients.${id}`, {
263
+ native: { token, tokenExpiresAt: record.tokenExpiresAt }
264
+ });
247
265
  }
248
266
  /**
249
267
  * Updates in-memory refresh token and persists to channel.native. Old refresh
@@ -448,12 +466,52 @@ class ClientRegistry {
448
466
  this.trackInMemory(record);
449
467
  await this.createObjects(record);
450
468
  this.touchLastSeen(record);
451
- this.adapter.log.info(ip ? `New client connected: ${id} (${hostname != null ? hostname : ip})` : `New client connected: ${id}`);
469
+ this.adapter.log.info(
470
+ ip ? `New client connected: ${id} (${(0, import_coerce.oneLine)(hostname != null ? hostname : ip)})` : `New client connected: ${id}`
471
+ );
452
472
  if (ip) {
453
473
  this.recordNewClientIp(ip);
454
474
  }
455
475
  return record;
456
476
  }
477
+ /**
478
+ * True when `ip` has already minted {@link NEW_CLIENT_THROTTLE_PER_HOUR} new
479
+ * clients in the current rolling hour — {@link recordNewClientIp} owns the
480
+ * per-IP burst window. Once true, `identifyOrCreate` hands out transient
481
+ * records (no object) until the window resets one hour after the burst began.
482
+ *
483
+ * @param ip Remote IP to check.
484
+ */
485
+ isIpThrottled(ip) {
486
+ const entry = this.newClientBurst.get(ip);
487
+ if (!entry || Date.now() - entry.since > 60 * 60 * 1e3) {
488
+ return false;
489
+ }
490
+ return entry.count >= import_constants.NEW_CLIENT_THROTTLE_PER_HOUR;
491
+ }
492
+ /**
493
+ * A non-persisted, untracked client handed out when an IP is over the
494
+ * new-client throttle. No `clients.<id>` object is created and the record is
495
+ * not added to any lookup map, so a cookieless spray cannot grow the object
496
+ * DB. Mode is the normal new-client default, so a legitimate-but-throttled
497
+ * client (e.g. behind a busy NAT) still resolves to the configured dashboard
498
+ * — it just doesn't get a persistent identity.
499
+ *
500
+ * @param ip Remote IP (advisory).
501
+ * @param hostname Reverse-DNS hostname, if any.
502
+ */
503
+ transientRecord(ip, hostname) {
504
+ return {
505
+ id: (0, import_network.generateClientId)(),
506
+ cookie: import_node_crypto.default.randomUUID(),
507
+ token: null,
508
+ refreshToken: null,
509
+ mode: this.newClientModeProvider(),
510
+ manualUrl: null,
511
+ ip,
512
+ hostname
513
+ };
514
+ }
457
515
  /**
458
516
  * v1.19.0 (G5): tracking-only — wenn eine IP > 3 neue Clients pro Stunde
459
517
  * erzeugt, einmaliger warn-log mit Diagnose-Hinweis. Danach 1h cooldown
@@ -633,11 +691,17 @@ class ClientRegistry {
633
691
  ]);
634
692
  }
635
693
  async updateIpHostname(record, ip, hostname) {
694
+ var _a;
636
695
  if (ip && ip !== record.ip) {
696
+ const previousIp = record.ip;
637
697
  record.ip = ip;
638
698
  await this.adapter.setStateAsync(`clients.${record.id}.ip`, { val: ip, ack: true });
639
699
  if (!record.hostname) {
640
- await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: ip } });
700
+ const obj = await this.adapter.getObjectAsync(`clients.${record.id}`);
701
+ const currentName = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.name;
702
+ if (currentName === void 0 || typeof currentName === "string" && currentName === previousIp) {
703
+ await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: ip } });
704
+ }
641
705
  }
642
706
  }
643
707
  if (hostname && hostname !== record.hostname) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/client-registry.ts"],
4
- "sourcesContent": ["/**\n * Client Registry \u2014 persistent multi-client store.\n *\n * Each client gets a channel `clients.<id>` with native.cookie / native.token\n * and states mode / manualUrl / ip / remove. Cookie is the primary identity\n * (auto-sent by browsers on page navigation), IP is only advisory.\n *\n * Registry state is dual-homed: in-memory maps for hot lookups, ioBroker\n * objects for persistence and user-visible config.\n */\n\nimport crypto from \"node:crypto\";\nimport {\n buildDropdownStates,\n coerceSafeUrl,\n coerceString,\n coerceUuid,\n evictOldest,\n isPlainObject,\n parseAdapterStateId,\n parseManualUrlWrite,\n parseModeWrite,\n safeGetState,\n} from \"./coerce\";\nimport { MODE_GLOBAL, MODE_MANUAL, NEW_CLIENT_BURST_CAP } from \"./constants\";\nimport { resolveLabel, tName } from \"./i18n\";\nimport { generateClientId } from \"./network\";\nimport type { AdapterInterface, ClientRecord, UrlStates } from \"./types\";\n\n/** Extended adapter interface for registry \u2014 needs object and state operations. */\nexport type RegistryAdapter = AdapterInterface &\n Pick<\n ioBroker.Adapter,\n | \"namespace\"\n | \"getForeignObjectsAsync\"\n | \"getStateAsync\"\n | \"getObjectAsync\"\n | \"setObjectNotExistsAsync\"\n | \"setObjectAsync\"\n | \"extendObjectAsync\"\n | \"setStateAsync\"\n | \"delObjectAsync\"\n >;\n\nconst CLIENTS_PREFIX = \"clients.\";\n\n/** Provides the default mode value for a freshly created client. */\nexport type NewClientModeProvider = () => string;\n\n/** Persistent multi-client store: cookie \u2192 channel, with in-memory lookup maps. */\nexport class ClientRegistry {\n private readonly adapter: RegistryAdapter;\n private readonly byCookie = new Map<string, ClientRecord>();\n private readonly byId = new Map<string, ClientRecord>();\n private readonly byToken = new Map<string, ClientRecord>();\n private readonly byRefreshToken = new Map<string, ClientRecord>();\n private currentUrlStates: UrlStates = {};\n private newClientModeProvider: NewClientModeProvider = () => MODE_GLOBAL;\n /**\n * In-flight client creations keyed by remote IP. Keeps parallel cookieless\n * requests from the same display (typical on first connect: HA clients fire\n * `GET /`, `GET /api/`, `POST /auth/login_flow` almost simultaneously) from\n * each creating a separate client record. The first request starts the\n * create; parallel requests await the same Promise and receive the same\n * client + cookie.\n */\n private readonly pendingByIp = new Map<string, Promise<ClientRecord>>();\n /**\n * Throttle for lastSeen-updates per client. Keyed by client id, value is the\n * last `Date.now()` we wrote `native.lastSeen` to ioBroker. Throttle window\n * is one hour \u2014 saves us extendObject roundtrips on every request.\n */\n private readonly lastSeenFlushedAt = new Map<string, number>();\n /**\n * v1.19.0 (G5): per-IP burst tracking f\u00FCr broken-cookie-Displays.\n * Wenn eine IP > 3 neue Clients in einer Stunde erzeugt, kommt ein\n * einmaliger warn-log mit Hinweis (cookie-Persistenz auf Display kaputt).\n */\n private readonly newClientBurst = new Map<string, { count: number; since: number; warnedAt: number }>();\n\n /** @param adapter Adapter instance used for object/state I/O. */\n constructor(adapter: RegistryAdapter) {\n this.adapter = adapter;\n }\n\n /**\n * Wires the default-mode provider used when a new client is registered.\n * Called from main.ts once registry, globalConfig and urlDiscovery exist.\n *\n * @param provider Function returning the desired default mode for a new client.\n */\n setNewClientModeProvider(provider: NewClientModeProvider): void {\n this.newClientModeProvider = provider;\n }\n\n /** Loads existing clients from ioBroker objects into memory. Call once on adapter start. */\n async restore(): Promise<void> {\n let channels: Record<string, ioBroker.ChannelObject> = {};\n try {\n channels = (await this.adapter.getForeignObjectsAsync(`${this.adapter.namespace}.clients.*`, \"channel\")) ?? {};\n } catch (err) {\n this.adapter.log.debug(`client-registry: restore failed: ${String(err)}`);\n return;\n }\n\n for (const [fullId, obj] of Object.entries(channels)) {\n const id = fullId.substring(`${this.adapter.namespace}.clients.`.length);\n if (!id || id.includes(\".\")) {\n continue;\n }\n // v1.28.3 (HE1): per-client try/catch \u2014 bei Promise.all auf 4\n // readState-Calls reicht ein einzelner Reject (z.B. corrupted state\n // bei migrating jsonl-store), und ALLE folgenden Clients werden\n // nicht restored. Pro-Client-Wrap entkoppelt: ein broken Client\n // kostet nur sich selbst.\n try {\n const native = isPlainObject(obj.native) ? obj.native : {};\n const cookie = coerceUuid(native.cookie);\n if (!cookie) {\n continue;\n }\n // v1.9.0 (D8): vier readState-Calls parallel statt sequenziell.\n // Mit 50 Clients waren das vorher 200 sequenzielle Round-Trips\n // bevor der WebServer up war; jetzt 50 parallele 4er-Gruppen.\n const [modeRaw, manualUrlRaw, ipRaw, hostnameRaw] = await Promise.all([\n this.readState(`${id}.mode`),\n this.readState(`${id}.manualUrl`),\n this.readState(`${id}.ip`),\n this.readState(`${id}.hostname`),\n ]);\n const mode = typeof modeRaw === \"string\" ? modeRaw : \"\";\n const manualUrl = coerceSafeUrl(manualUrlRaw);\n const ip = coerceString(ipRaw);\n const token = coerceUuid(native.token);\n const refreshToken = coerceUuid(native.refreshToken);\n\n // Legacy migration (<=1.1.1): hostname lived in its own state. If present,\n // move the value into common.name and drop the state.\n const legacyHostname = coerceString(hostnameRaw);\n let channelName = coerceString(obj.common?.name);\n if (legacyHostname) {\n this.adapter.log.debug(\n `restore: legacy hostname migration for client ${id} \u2014 '${legacyHostname}' moved to common.name`,\n );\n if (legacyHostname !== channelName) {\n await this.adapter.extendObjectAsync(`clients.${id}`, { common: { name: legacyHostname } });\n channelName = legacyHostname;\n }\n try {\n await this.adapter.delObjectAsync(`clients.${id}.hostname`);\n } catch {\n /* best effort \u2014 ignore */\n }\n }\n const hostname = channelName && channelName !== ip && channelName !== id ? channelName : null;\n\n const record: ClientRecord = { id, cookie, token, refreshToken, mode, manualUrl, ip, hostname };\n this.trackInMemory(record);\n // Legacy clients (v1.1.x) only had `visUrl` + `ip` + `remove` objects;\n // ensure the v1.2.0+ objects (`mode`, `manualUrl`) exist before any\n // state writes from migration land \u2014 otherwise js-controller logs\n // \"State has no existing object\" warnings.\n await this.ensureObjects(record);\n // Promote blank state-value to numeric 0 so the dropdown renders\n // the `0='---'` option as selected. v1.2.0 installs left the value\n // as `''` which doesn't match any common.states entry.\n const modeStateRaw = await this.readState(`${id}.mode`);\n if (modeStateRaw === \"\" || modeStateRaw === null || modeStateRaw === undefined) {\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n }\n } catch (err) {\n this.adapter.log.debug(`client-registry: skipping ${id} during restore \u2014 ${String(err)}`);\n }\n }\n this.adapter.log.debug(`client-registry: restored ${this.byId.size} client(s)`);\n }\n\n /**\n * Find the client for this cookie or create a new one.\n * Creates channel + states on first call and updates IP/hostname if changed.\n *\n * @param cookie Incoming cookie value (may be null/invalid).\n * @param ip Remote IP observed by the server.\n * @param hostname Optional hostname (from reverse DNS), stored for the admin UI.\n * @param userAgent Optional User-Agent header for NAT-collision-Schutz im Pending-Lock.\n */\n async identifyOrCreate(\n cookie: string | null,\n ip: string | null,\n hostname: string | null,\n userAgent: string | null = null,\n ): Promise<ClientRecord> {\n const validCookie = coerceUuid(cookie);\n if (validCookie) {\n const existing = this.byCookie.get(validCookie);\n if (existing) {\n await this.updateIpHostname(existing, ip, hostname);\n this.touchLastSeen(existing);\n return existing;\n }\n }\n // No valid cookie: before spinning up a new client, check whether this\n // IP already has a create in flight. If so, await that Promise \u2014 the\n // parallel request of the same display's initial burst will get the\n // same cookie + client, no more duplicate \"New client\" log entries.\n //\n // v1.17.0 (C8): Bucket-Key kombiniert IP + User-Agent-Hash, sodass\n // zwei verschiedene Displays hinter derselben NAT-IP NICHT in denselben\n // Pending-Lock fallen (vorher: gleicher Cookie/Token/Mode \u2192 Cookie-\n // Klau-Vektor). UA-Hash truncated auf 12 Hex-Chars um Memory-Footprint\n // klein zu halten. Bei UA=null f\u00E4llt der Bucket auf reines IP zur\u00FCck.\n if (ip) {\n const bucketKey = userAgent\n ? `${ip}|${crypto.createHash(\"sha256\").update(userAgent).digest(\"hex\").substring(0, 12)}`\n : ip;\n const pending = this.pendingByIp.get(bucketKey);\n if (pending) {\n // v1.21.0 (D3): pending-promise kann rejecten \u2014 z.B. wenn\n // createClient async failed (broker-disconnect, object-create-\n // error). Wir k\u00F6nnen nicht recover'n (der erste Caller hat eh\n // schon gefailt), aber wir wollen den Fehler diagnostizierbar\n // machen. catch+rethrow sorgt f\u00FCr ein einzelnes log statt\n // unhandled-rejection im fastify-error-handler.\n return pending.catch(err => {\n this.adapter.log.debug(`client-registry: pending createClient for ${bucketKey} rejected: ${String(err)}`);\n throw err;\n });\n }\n const promise = this.createClient(ip, hostname);\n this.pendingByIp.set(bucketKey, promise);\n try {\n return await promise;\n } catch (err) {\n // Tech-Diagnose mit Stack-Detail \u2014 bleibt debug (Maintainer-only).\n this.adapter.log.debug(\n `client-registry: createClient failed for IP ${ip}: ${err instanceof Error ? err.message : String(err)}`,\n );\n throw err;\n } finally {\n this.pendingByIp.delete(bucketKey);\n }\n }\n return this.createClient(ip, hostname);\n }\n\n /**\n * Lookup by short client id (channel segment).\n *\n * @param id Client id.\n */\n getById(id: string): ClientRecord | null {\n return this.byId.get(id) ?? null;\n }\n\n /**\n * Lookup by cookie value. Invalid UUIDs return null.\n *\n * @param cookie Raw cookie string.\n */\n getByCookie(cookie: string): ClientRecord | null {\n const v = coerceUuid(cookie);\n return v ? (this.byCookie.get(v) ?? null) : null;\n }\n\n /**\n * Lookup by access token issued during the auth flow.\n *\n * @param token Bearer token.\n */\n getByToken(token: string): ClientRecord | null {\n return this.byToken.get(token) ?? null;\n }\n\n /**\n * Lookup by refresh token issued during the auth flow.\n *\n * @param refreshToken Refresh token value.\n */\n getByRefreshToken(refreshToken: string): ClientRecord | null {\n return this.byRefreshToken.get(refreshToken) ?? null;\n }\n\n /** Returns a snapshot array of all registered clients. */\n listAll(): ClientRecord[] {\n return [...this.byId.values()];\n }\n\n /**\n * Updates in-memory token and persists to channel.native. Old token is freed.\n *\n * @param id Client id.\n * @param token New bearer token, or null to clear.\n */\n async setToken(id: string, token: string | null): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n if (record.token) {\n this.byToken.delete(record.token);\n }\n record.token = token;\n if (token) {\n this.byToken.set(token, record);\n }\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { token } });\n }\n\n /**\n * Updates in-memory refresh token and persists to channel.native. Old refresh\n * token is freed. Stored plain-text in `clients.<id>.native.refreshToken` \u2014\n * same exposure profile as the access token (see {@link ClientRecord.refreshToken}).\n *\n * @param id Client id.\n * @param refreshToken New refresh token, or null to clear.\n */\n async setRefreshToken(id: string, refreshToken: string | null): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n if (record.refreshToken) {\n this.byRefreshToken.delete(record.refreshToken);\n }\n record.refreshToken = refreshToken;\n if (refreshToken) {\n this.byRefreshToken.set(refreshToken, record);\n }\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { refreshToken } });\n }\n\n /**\n * Accept an external mode write on `clients.<id>.mode`.\n *\n * Allowed values: `'global'`, `'manual'`, or any URL that passes\n * {@link coerceSafeUrl}. Empty string clears the choice \u2192 setup page.\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleModeWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n // v1.23.0 (F2): zentralisierte Validierung via parseModeWrite. Vorher\n // hatten client-registry und global-config ~80% identische Logik\n // (no-choice, non-string, sentinel, URL-coerce) dupliziert.\n const result = parseModeWrite(rawValue, [MODE_GLOBAL, MODE_MANUAL]);\n switch (result.kind) {\n case \"no-choice\":\n record.mode = \"\";\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n this.adapter.log.debug(`Client ${id}: mode \u2192 cleared (no-choice)`);\n return;\n case \"rejected-non-string\":\n // v1.18.0 (G7): debug statt warn \u2014 nicht-string mode-Schreibungen\n // sind UI-Echo, kein Server-Concern.\n this.adapter.log.debug(`client-registry: rejected non-string mode for ${id}`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n return;\n case \"sentinel\":\n if (result.value === MODE_MANUAL && !record.manualUrl) {\n this.adapter.log.warn(\n `Client ${id}: mode set to \"manual\" but manualUrl is empty \u2014 fill clients.${id}.manualUrl to redirect`,\n );\n }\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n this.adapter.log.debug(`Client ${id}: mode \u2192 '${result.value}' (sentinel)`);\n return;\n case \"rejected-unsafe-url\":\n this.adapter.log.warn(`Client ${id}: rejected unsafe mode value \"${result.raw}\"`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode, ack: true });\n return;\n case \"url\":\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n this.adapter.log.debug(`Client ${id}: mode \u2192 ${result.value} (direct URL)`);\n return;\n // 'rejected-disallowed-sentinel' kommt hier nicht vor weil beide\n // Sentinels (global/manual) erlaubt sind. Defensive: revert.\n default:\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n }\n }\n\n /**\n * Accept an external manualUrl write on `clients.<id>.manualUrl`.\n * Free-text \u2014 must pass {@link coerceSafeUrl} or be empty (clears).\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleManualUrlWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n const result = parseManualUrlWrite(rawValue);\n if (!result.ok) {\n this.adapter.log.warn(`Client ${id}: rejected unsafe manualUrl value`);\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: record.manualUrl ?? \"\", ack: true });\n return;\n }\n record.manualUrl = result.safe;\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: result.safe ?? \"\", ack: true });\n this.adapter.log.debug(`Client ${id}: manualUrl \u2192 ${result.safe ?? \"cleared\"}`);\n if (record.mode === MODE_MANUAL && !result.safe) {\n this.adapter.log.warn(`Client ${id}: manualUrl cleared while mode is \"manual\" \u2014 display will see the setup page`);\n }\n }\n\n /**\n * Set every client's `mode` to the same value. Used by the master switch\n * (`global.enabled`) to bulk-sync all displays \u2014 `'global'` when on,\n * `'0'` (no-choice \u2192 landing page) when off.\n *\n * Skips clients whose mode already matches (no spurious state writes).\n *\n * @param value New mode value (sentinel or URL).\n */\n async bulkSetMode(value: string): Promise<void> {\n // v1.8.1 (D7): parallele setStateAsync statt sequenziell. Mit 50 Displays\n // war das vorher 50 Broker-Round-Trips. setStateAsync ist Broker-internal,\n // Parallelism ist safe.\n const writes: Array<Promise<unknown>> = [];\n let changed = 0;\n for (const record of this.byId.values()) {\n if (record.mode === value) {\n continue;\n }\n record.mode = value;\n writes.push(this.adapter.setStateAsync(`clients.${record.id}.mode`, { val: value, ack: true }));\n changed++;\n }\n if (writes.length > 0) {\n await Promise.all(writes);\n }\n if (changed > 0) {\n this.adapter.log.debug(`bulkSetMode applied to ${changed} client(s)`);\n }\n }\n\n /**\n * Removes the client entirely \u2014 channel + states deleted, next visit creates a new entry.\n *\n * @param id Client id to forget.\n */\n async remove(id: string): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n this.byId.delete(id);\n this.byCookie.delete(record.cookie);\n if (record.token) {\n this.byToken.delete(record.token);\n }\n if (record.refreshToken) {\n this.byRefreshToken.delete(record.refreshToken);\n }\n // v1.8.1 (D2): lastSeenFlushedAt war fr\u00FCher nicht aufger\u00E4umt \u2014 bei\n // ID-Reuse (16M-Space, m\u00F6glich nach 100+ Clients \u00FCber Jahre) h\u00E4tte\n // die alte Throttle-Entry den ersten lastSeen-Write des neuen Clients\n // inhibiert. Plus minimal Memory-Leak.\n this.lastSeenFlushedAt.delete(id);\n try {\n await this.adapter.delObjectAsync(`clients.${id}`, { recursive: true });\n } catch (err) {\n // Stack-trace level \u2014 Maintainer-Diagnose, EN bleibt.\n this.adapter.log.debug(`client-registry: delObject failed for ${id}: ${String(err)}`);\n }\n this.adapter.log.info(`Client forgotten: ${id}`);\n }\n\n /**\n * Updates the mode dropdown states (`common.states`) on every client's mode datapoint.\n * Adds the `'global'` and `'manual'` sentinels on top of the discovered URLs.\n *\n * @param states Discovered URL \u2192 label map.\n */\n async syncUrlDropdown(states: UrlStates): Promise<void> {\n this.currentUrlStates = states;\n const merged = this.buildModeStates();\n // v1.27.2: extendObjectAsync mergt `common.states` tief \u2014 alte\n // URL-Schl\u00FCssel die nicht mehr discovered werden, blieben sonst im\n // Dropdown stehen (sichtbar nach v1.26\u2192v1.27 URL-Format-Wechsel:\n // alte `vis-2.0/main/index.html`-Keys neben neuen `vis-2/index.html?main`).\n // Object lesen, common.states komplett ersetzen, dann setObjectAsync.\n // v1.30.0 (R4): pro-Client get+set parallel statt sequentiell. Analog\n // gcStaleClients in main.ts (v1.28.3 M5). Sp\u00FCrbar bei Display-Farmen\n // mit 30+ Clients \u2014 vorher 2\u00D7N Broker-Round-Trips sequenziell.\n await Promise.all(\n Array.from(this.byId.keys()).map(async id => {\n const stateId = `clients.${id}.mode`;\n const existing = await this.adapter.getObjectAsync(stateId);\n if (!existing) {\n return;\n }\n existing.common.states = merged;\n await this.adapter.setObjectAsync(stateId, existing);\n }),\n );\n }\n\n // --- internal ---\n\n private trackInMemory(record: ClientRecord): void {\n this.byId.set(record.id, record);\n this.byCookie.set(record.cookie, record);\n if (record.token) {\n this.byToken.set(record.token, record);\n }\n if (record.refreshToken) {\n this.byRefreshToken.set(record.refreshToken, record);\n }\n }\n\n private async createClient(ip: string | null, hostname: string | null): Promise<ClientRecord> {\n let id = generateClientId();\n while (this.byId.has(id)) {\n id = generateClientId();\n }\n const cookie = crypto.randomUUID();\n const mode = this.newClientModeProvider();\n const record: ClientRecord = {\n id,\n cookie,\n token: null,\n refreshToken: null,\n mode,\n manualUrl: null,\n ip,\n hostname,\n };\n this.trackInMemory(record);\n await this.createObjects(record);\n this.touchLastSeen(record);\n this.adapter.log.info(ip ? `New client connected: ${id} (${hostname ?? ip})` : `New client connected: ${id}`);\n // v1.19.0 (G5): IP-Burst-Detection f\u00FCr broken-cookie-Displays. Wenn\n // dieselbe IP > 3 neue Clients in einer Stunde erzeugt, ist der Cookie-\n // Mechanismus auf dem Display kaputt (aggressive Privacy, refresh-bug).\n // Einmaliger warn-Hinweis pro IP pro Stunde \u2014 danach bleibt der info-\n // log normal, aber der Operator hat wenigstens einen Anker zur Diagnose.\n if (ip) {\n this.recordNewClientIp(ip);\n }\n return record;\n }\n\n /**\n * v1.19.0 (G5): tracking-only \u2014 wenn eine IP > 3 neue Clients pro Stunde\n * erzeugt, einmaliger warn-log mit Diagnose-Hinweis. Danach 1h cooldown\n * pro IP. Map-Cap 200 (FIFO).\n *\n * @param ip Remote IP that just got a new ClientRecord assigned.\n */\n private recordNewClientIp(ip: string): void {\n const now = Date.now();\n const HOUR = 60 * 60 * 1000;\n const entry = this.newClientBurst.get(ip) ?? { count: 0, since: now, warnedAt: 0 };\n if (now - entry.since > HOUR) {\n // Window expired \u2014 reset.\n entry.count = 0;\n entry.since = now;\n }\n entry.count += 1;\n if (entry.count > 3 && now - entry.warnedAt > HOUR) {\n this.adapter.log.warn(\n `IP ${ip} created ${entry.count} clients within an hour \u2014 display likely is not persisting cookies (privacy mode? refresh bug?)`,\n );\n entry.warnedAt = now;\n }\n this.newClientBurst.set(ip, entry);\n // v1.32.0: Soft-Cap via shared `evictOldest` (vorher inline single-shot).\n evictOldest(this.newClientBurst, NEW_CLIENT_BURST_CAP);\n }\n\n /**\n * Updates `native.lastSeen` on the channel, throttled to once per hour per\n * client. Used for the stale-client-GC: clients without token + lastSeen\n * older than 30 days get auto-removed on adapter start.\n *\n * Fire-and-forget \u2014 failures only debug-logged.\n *\n * @param record Client whose lastSeen-timestamp should be refreshed.\n */\n private touchLastSeen(record: ClientRecord): void {\n const now = Date.now();\n const last = this.lastSeenFlushedAt.get(record.id) ?? 0;\n if (now - last < 60 * 60 * 1000) {\n return; // throttle: 1\u00D7 per hour\n }\n this.lastSeenFlushedAt.set(record.id, now);\n this.adapter\n .extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } })\n .catch(err => this.adapter.log.debug(`touchLastSeen failed for ${record.id}: ${String(err)}`));\n }\n\n /**\n * v1.19.0 (F11): zentraler lastSeen-Seed-Pfad. Vorher hatte main.ts\n * gcStaleClients seinen eigenen extendObjectAsync-Call mit identischem\n * native-Format \u2014 DRY-Violation und gef\u00E4hrlich wenn das Format mal \u00E4ndert.\n * Jetzt nutzen beide Pfade diese Methode. Throttle-Map wird auch upgedated,\n * damit der n\u00E4chste touchLastSeen den seed nicht direkt \u00FCberschreibt.\n *\n * @param id Client id (short segment, ohne `clients.`-Prefix).\n * @param now Optionaler Timestamp f\u00FCr tests; default Date.now().\n */\n async seedLastSeen(id: string, now: number = Date.now()): Promise<void> {\n this.lastSeenFlushedAt.set(id, now);\n try {\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { lastSeen: now } });\n } catch (err) {\n this.adapter.log.debug(`seedLastSeen failed for ${id}: ${String(err)}`);\n }\n }\n\n /**\n * Builds the dropdown-states map for `clients.<id>.mode`. Includes the\n * `0='---'` no-choice fallback (analogous to the govee-smart pattern), the\n * `'global'` + `'manual'` sentinels, and all currently discovered URLs.\n */\n private buildModeStates(): UrlStates {\n // v1.20.0 (F4): Helper aus coerce.ts. Vorher dupliziert mit global-config\n // (bis auf den zus\u00E4tzlichen `global`-Sentinel hier, weil clients per\n // `mode='global'` an global delegieren k\u00F6nnen \u2014 global selbst nicht).\n // v1.28.4: Sentinel-Labels als plain-string in adapter.systemLanguage \u2014\n // Admin rendert common.states-VALUES direkt als React-child und crasht\n // bei Translation-Objects mit React Error #31. v1.28.0 hatte einen\n // helper hier via `as unknown as string`-Cast eingeschleust, der den\n // Type-Check still lie\u00DF aber den Admin-Dropdown beim \u00D6ffnen zerlegt\n // hat. Memory `reference_common_states_plain_string_only`.\n return buildDropdownStates(\n {\n [MODE_GLOBAL]: resolveLabel(\"globalUrl\"),\n [MODE_MANUAL]: resolveLabel(\"manualUrl\"),\n },\n this.currentUrlStates,\n );\n }\n\n /**\n * Idempotently creates all per-client objects (channel + states). Safe to\n * call repeatedly \u2014 uses `setObjectNotExistsAsync` everywhere. Called from\n * both `restore()` (so legacy v1.1.x clients gain the new mode/manualUrl\n * objects before migration writes states) and `createClient()`.\n *\n * @param record Client to create or ensure objects for.\n */\n private async ensureObjects(record: ClientRecord): Promise<void> {\n const { id, cookie, ip, hostname } = record;\n const mergedStates = this.buildModeStates();\n\n // Channel: setObjectNotExistsAsync \u2014 common.name is updated dynamically\n // by updateIpHostname() when reverse-DNS resolves; we must not clobber it.\n await this.adapter.setObjectNotExistsAsync(`clients.${id}`, {\n type: \"channel\",\n common: { name: hostname ?? ip ?? id },\n native: { cookie, token: null },\n });\n\n // States:\n // - `clients.<id>.mode` uses get+setObjectAsync (analog `global-config.ts`\n // v1.27.2-Fix): extendObjectAsync deep-merges common.states, weshalb\n // alte i18n-Object-Keys aus pre-v1.28.4-Installs unter den gleichen\n // keys h\u00E4ngen blieben \u2192 React Error #31 beim Admin-Dropdown-Open.\n // setObjectAsync ersetzt common komplett (custom-Custom-Subscriptions\n // wie influxdb.0 bleiben via spread aus existing erhalten).\n // - Repair-Pfad f\u00FCr partial-formed Objects (v1.2.0-Migration-Bug, common\n // ohne top-level type/name/role) ist auch abgedeckt: existing-fields\n // werden von der full schema common komplett \u00FCberschrieben.\n // - `clients.<id>.manualUrl` bleibt extendObjectAsync \u2014 kein states-Feld,\n // daher kein i18n-Object-Risiko.\n const modeFullCommon: ioBroker.StateCommon = {\n // tName returns StringOrTranslated, which common.name accepts directly.\n name: tName(\"clientMode\"),\n // 'mixed' future-proofs against the upcoming js-controller\n // strict-type cast (see govee-smart v1.11.0 pattern).\n type: \"mixed\",\n role: \"state\",\n read: true,\n write: true,\n def: 0,\n states: mergedStates,\n };\n const ensureModeObject = async (): Promise<void> => {\n const existing = await this.adapter.getObjectAsync(`clients.${id}.mode`);\n if (existing) {\n const preservedName = existing.common?.name;\n existing.common = { ...existing.common, ...modeFullCommon };\n if (preservedName !== undefined) {\n existing.common.name = preservedName;\n }\n existing.type = \"state\";\n await this.adapter.setObjectAsync(`clients.${id}.mode`, existing);\n } else {\n await this.adapter.setObjectAsync(`clients.${id}.mode`, {\n type: \"state\",\n common: modeFullCommon,\n native: {},\n });\n }\n };\n await Promise.all([\n ensureModeObject(),\n this.adapter.extendObjectAsync(\n `clients.${id}.manualUrl`,\n {\n type: \"state\",\n common: {\n name: tName(\"clientManualUrl\"),\n type: \"string\",\n role: \"url\",\n read: true,\n write: true,\n def: \"\",\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n ),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.ip`, {\n type: \"state\",\n common: {\n name: tName(\"clientIp\"),\n type: \"string\",\n role: \"info.ip\",\n read: true,\n write: false,\n def: \"\",\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.remove`, {\n type: \"state\",\n common: {\n name: tName(\"clientRemove\"),\n type: \"boolean\",\n role: \"button\",\n read: false,\n write: true,\n def: false,\n },\n native: {},\n }),\n ]);\n }\n\n private async createObjects(record: ClientRecord): Promise<void> {\n await this.ensureObjects(record);\n const { id, mode, ip } = record;\n await Promise.all([\n this.adapter.setStateAsync(`clients.${id}.ip`, { val: ip ?? \"\", ack: true }),\n this.adapter.setStateAsync(`clients.${id}.mode`, { val: mode, ack: true }),\n this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: \"\", ack: true }),\n ]);\n }\n\n private async updateIpHostname(record: ClientRecord, ip: string | null, hostname: string | null): Promise<void> {\n if (ip && ip !== record.ip) {\n record.ip = ip;\n await this.adapter.setStateAsync(`clients.${record.id}.ip`, { val: ip, ack: true });\n // If no hostname known yet, common.name falls back to the IP \u2014 keep it current.\n if (!record.hostname) {\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: ip } });\n }\n }\n if (hostname && hostname !== record.hostname) {\n record.hostname = hostname;\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: hostname } });\n }\n }\n\n private async readState(subId: string): Promise<unknown> {\n // v1.20.0 (F10): Helper aus coerce.ts \u2014 vorher dupliziert mit\n // global-config.safeGetState (gleicher try/catch-+-null-Fallback,\n // nur Pfad-Prefix anders).\n const s = await safeGetState(this.adapter, `clients.${subId}`);\n return s?.val ?? null;\n }\n}\n\n/**\n * Check whether a full state ID matches a client control datapoint and extract id + kind.\n *\n * @param fullId The full state id from a state change event.\n * @param namespace The adapter namespace (e.g. `hassemu.0`).\n */\nexport function parseClientStateId(\n fullId: string,\n namespace: string,\n): { id: string; kind: \"mode\" | \"manualUrl\" | \"remove\" } | null {\n // v1.20.0 (F9): generischer parseAdapterStateId-Helper. Vorher hatte\n // client-registry seine eigene Prefix-+-Tail-Validierung dupliziert mit\n // global-config.parseGlobalStateId.\n const parts = parseAdapterStateId(fullId, namespace, CLIENTS_PREFIX, 2);\n if (!parts) {\n return null;\n }\n const [id, kind] = parts;\n // v1.9.0 (E5): empty id rejection (`clients..mode` would parse to id='').\n if (!id) {\n return null;\n }\n if (kind !== \"mode\" && kind !== \"manualUrl\" && kind !== \"remove\") {\n return null;\n }\n return { id, kind };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Client Registry \u2014 persistent multi-client store.\n *\n * Each client gets a channel `clients.<id>` with native.cookie / native.token\n * and states mode / manualUrl / ip / remove. Cookie is the primary identity\n * (auto-sent by browsers on page navigation), IP is only advisory.\n *\n * Registry state is dual-homed: in-memory maps for hot lookups, ioBroker\n * objects for persistence and user-visible config.\n */\n\nimport crypto from \"node:crypto\";\nimport {\n buildDropdownStates,\n coerceSafeUrl,\n coerceString,\n coerceUuid,\n evictOldest,\n isPlainObject,\n oneLine,\n parseAdapterStateId,\n parseManualUrlWrite,\n parseModeWrite,\n safeGetState,\n} from \"./coerce\";\nimport {\n MODE_GLOBAL,\n MODE_MANUAL,\n NEW_CLIENT_BURST_CAP,\n NEW_CLIENT_THROTTLE_PER_HOUR,\n OAUTH_ACCESS_TOKEN_TTL_S,\n} from \"./constants\";\nimport { resolveLabel, tName } from \"./i18n\";\nimport { generateClientId } from \"./network\";\nimport type { AdapterInterface, ClientRecord, UrlStates } from \"./types\";\n\n/** Extended adapter interface for registry \u2014 needs object and state operations. */\nexport type RegistryAdapter = AdapterInterface &\n Pick<\n ioBroker.Adapter,\n | \"namespace\"\n | \"getForeignObjectsAsync\"\n | \"getStateAsync\"\n | \"getObjectAsync\"\n | \"setObjectNotExistsAsync\"\n | \"setObjectAsync\"\n | \"extendObjectAsync\"\n | \"setStateAsync\"\n | \"delObjectAsync\"\n >;\n\nconst CLIENTS_PREFIX = \"clients.\";\n\n/** Provides the default mode value for a freshly created client. */\nexport type NewClientModeProvider = () => string;\n\n/** Persistent multi-client store: cookie \u2192 channel, with in-memory lookup maps. */\nexport class ClientRegistry {\n private readonly adapter: RegistryAdapter;\n private readonly byCookie = new Map<string, ClientRecord>();\n private readonly byId = new Map<string, ClientRecord>();\n private readonly byToken = new Map<string, ClientRecord>();\n private readonly byRefreshToken = new Map<string, ClientRecord>();\n private currentUrlStates: UrlStates = {};\n private newClientModeProvider: NewClientModeProvider = () => MODE_GLOBAL;\n /**\n * In-flight client creations keyed by remote IP. Keeps parallel cookieless\n * requests from the same display (typical on first connect: HA clients fire\n * `GET /`, `GET /api/`, `POST /auth/login_flow` almost simultaneously) from\n * each creating a separate client record. The first request starts the\n * create; parallel requests await the same Promise and receive the same\n * client + cookie.\n */\n private readonly pendingByIp = new Map<string, Promise<ClientRecord>>();\n /**\n * Throttle for lastSeen-updates per client. Keyed by client id, value is the\n * last `Date.now()` we wrote `native.lastSeen` to ioBroker. Throttle window\n * is one hour \u2014 saves us extendObject roundtrips on every request.\n */\n private readonly lastSeenFlushedAt = new Map<string, number>();\n /**\n * v1.19.0 (G5): per-IP burst tracking f\u00FCr broken-cookie-Displays.\n * Wenn eine IP > 3 neue Clients in einer Stunde erzeugt, kommt ein\n * einmaliger warn-log mit Hinweis (cookie-Persistenz auf Display kaputt).\n */\n private readonly newClientBurst = new Map<string, { count: number; since: number; warnedAt: number }>();\n\n /** @param adapter Adapter instance used for object/state I/O. */\n constructor(adapter: RegistryAdapter) {\n this.adapter = adapter;\n }\n\n /**\n * Wires the default-mode provider used when a new client is registered.\n * Called from main.ts once registry, globalConfig and urlDiscovery exist.\n *\n * @param provider Function returning the desired default mode for a new client.\n */\n setNewClientModeProvider(provider: NewClientModeProvider): void {\n this.newClientModeProvider = provider;\n }\n\n /** Loads existing clients from ioBroker objects into memory. Call once on adapter start. */\n async restore(): Promise<void> {\n let channels: Record<string, ioBroker.ChannelObject> = {};\n try {\n channels = (await this.adapter.getForeignObjectsAsync(`${this.adapter.namespace}.clients.*`, \"channel\")) ?? {};\n } catch (err) {\n this.adapter.log.debug(`client-registry: restore failed: ${String(err)}`);\n return;\n }\n\n for (const [fullId, obj] of Object.entries(channels)) {\n const id = fullId.substring(`${this.adapter.namespace}.clients.`.length);\n if (!id || id.includes(\".\")) {\n continue;\n }\n // v1.28.3 (HE1): per-client try/catch \u2014 bei Promise.all auf 4\n // readState-Calls reicht ein einzelner Reject (z.B. corrupted state\n // bei migrating jsonl-store), und ALLE folgenden Clients werden\n // nicht restored. Pro-Client-Wrap entkoppelt: ein broken Client\n // kostet nur sich selbst.\n try {\n const native = isPlainObject(obj.native) ? obj.native : {};\n const cookie = coerceUuid(native.cookie);\n if (!cookie) {\n continue;\n }\n // v1.9.0 (D8): vier readState-Calls parallel statt sequenziell.\n // Mit 50 Clients waren das vorher 200 sequenzielle Round-Trips\n // bevor der WebServer up war; jetzt 50 parallele 4er-Gruppen.\n const [modeRaw, manualUrlRaw, ipRaw, hostnameRaw] = await Promise.all([\n this.readState(`${id}.mode`),\n this.readState(`${id}.manualUrl`),\n this.readState(`${id}.ip`),\n this.readState(`${id}.hostname`),\n ]);\n const mode = typeof modeRaw === \"string\" ? modeRaw : \"\";\n const manualUrl = coerceSafeUrl(manualUrlRaw);\n const ip = coerceString(ipRaw);\n const token = coerceUuid(native.token);\n const refreshToken = coerceUuid(native.refreshToken);\n // v1.36.0 (S5): restore the persisted access-token expiry so a token that\n // already expired before the restart is rejected by getByToken on next use.\n const tokenExpiresAt = token && typeof native.tokenExpiresAt === \"number\" ? native.tokenExpiresAt : null;\n\n // Legacy migration (<=1.1.1): hostname lived in its own state. If present,\n // move the value into common.name and drop the state.\n const legacyHostname = coerceString(hostnameRaw);\n let channelName = coerceString(obj.common?.name);\n if (legacyHostname) {\n this.adapter.log.debug(\n `restore: legacy hostname migration for client ${id} \u2014 '${legacyHostname}' moved to common.name`,\n );\n if (legacyHostname !== channelName) {\n await this.adapter.extendObjectAsync(`clients.${id}`, { common: { name: legacyHostname } });\n channelName = legacyHostname;\n }\n try {\n await this.adapter.delObjectAsync(`clients.${id}.hostname`);\n } catch {\n /* best effort \u2014 ignore */\n }\n }\n const hostname = channelName && channelName !== ip && channelName !== id ? channelName : null;\n\n const record: ClientRecord = { id, cookie, token, tokenExpiresAt, refreshToken, mode, manualUrl, ip, hostname };\n this.trackInMemory(record);\n // Legacy clients (v1.1.x) only had `visUrl` + `ip` + `remove` objects;\n // ensure the v1.2.0+ objects (`mode`, `manualUrl`) exist before any\n // state writes from migration land \u2014 otherwise js-controller logs\n // \"State has no existing object\" warnings.\n await this.ensureObjects(record);\n // Promote blank state-value to numeric 0 so the dropdown renders\n // the `0='---'` option as selected. v1.2.0 installs left the value\n // as `''` which doesn't match any common.states entry.\n const modeStateRaw = await this.readState(`${id}.mode`);\n if (modeStateRaw === \"\" || modeStateRaw === null || modeStateRaw === undefined) {\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n }\n } catch (err) {\n this.adapter.log.debug(`client-registry: skipping ${id} during restore \u2014 ${String(err)}`);\n }\n }\n this.adapter.log.debug(`client-registry: restored ${this.byId.size} client(s)`);\n }\n\n /**\n * Find the client for this cookie or create a new one.\n * Creates channel + states on first call and updates IP/hostname if changed.\n *\n * @param cookie Incoming cookie value (may be null/invalid).\n * @param ip Remote IP observed by the server.\n * @param hostname Optional hostname (from reverse DNS), stored for the admin UI.\n * @param userAgent Optional User-Agent header for NAT-collision-Schutz im Pending-Lock.\n */\n async identifyOrCreate(\n cookie: string | null,\n ip: string | null,\n hostname: string | null,\n userAgent: string | null = null,\n ): Promise<ClientRecord> {\n const validCookie = coerceUuid(cookie);\n if (validCookie) {\n const existing = this.byCookie.get(validCookie);\n if (existing) {\n await this.updateIpHostname(existing, ip, hostname);\n this.touchLastSeen(existing);\n return existing;\n }\n }\n // No valid cookie: before spinning up a new client, check whether this\n // IP already has a create in flight. If so, await that Promise \u2014 the\n // parallel request of the same display's initial burst will get the\n // same cookie + client, no more duplicate \"New client\" log entries.\n //\n // v1.17.0 (C8): Bucket-Key kombiniert IP + User-Agent-Hash, sodass\n // zwei verschiedene Displays hinter derselben NAT-IP NICHT in denselben\n // Pending-Lock fallen (vorher: gleicher Cookie/Token/Mode \u2192 Cookie-\n // Klau-Vektor). UA-Hash truncated auf 12 Hex-Chars um Memory-Footprint\n // klein zu halten. Bei UA=null f\u00E4llt der Bucket auf reines IP zur\u00FCck.\n if (ip) {\n // Refuse to mint a new *persistent* client for an IP spraying cookieless\n // requests \u2014 hand out a transient (non-persisted, no object) record so the\n // ioBroker object DB cannot grow without bound. A real display keeps its\n // cookie and is identified above, so it never reaches this throttle.\n if (this.isIpThrottled(ip)) {\n this.adapter.log.debug(`identify: IP ${ip} over new-client throttle \u2014 serving a transient record (no object)`);\n return this.transientRecord(ip, hostname);\n }\n const bucketKey = userAgent\n ? `${ip}|${crypto.createHash(\"sha256\").update(userAgent).digest(\"hex\").substring(0, 12)}`\n : ip;\n const pending = this.pendingByIp.get(bucketKey);\n if (pending) {\n // v1.21.0 (D3): pending-promise kann rejecten \u2014 z.B. wenn\n // createClient async failed (broker-disconnect, object-create-\n // error). Wir k\u00F6nnen nicht recover'n (der erste Caller hat eh\n // schon gefailt), aber wir wollen den Fehler diagnostizierbar\n // machen. catch+rethrow sorgt f\u00FCr ein einzelnes log statt\n // unhandled-rejection im fastify-error-handler.\n return pending.catch(err => {\n this.adapter.log.debug(`client-registry: pending createClient for ${bucketKey} rejected: ${String(err)}`);\n throw err;\n });\n }\n const promise = this.createClient(ip, hostname);\n this.pendingByIp.set(bucketKey, promise);\n try {\n return await promise;\n } catch (err) {\n // Tech-Diagnose mit Stack-Detail \u2014 bleibt debug (Maintainer-only).\n this.adapter.log.debug(\n `client-registry: createClient failed for IP ${ip}: ${err instanceof Error ? err.message : String(err)}`,\n );\n throw err;\n } finally {\n this.pendingByIp.delete(bucketKey);\n }\n }\n return this.createClient(ip, hostname);\n }\n\n /**\n * Lookup by short client id (channel segment).\n *\n * @param id Client id.\n */\n getById(id: string): ClientRecord | null {\n return this.byId.get(id) ?? null;\n }\n\n /**\n * Lookup by cookie value. Invalid UUIDs return null.\n *\n * @param cookie Raw cookie string.\n */\n getByCookie(cookie: string): ClientRecord | null {\n const v = coerceUuid(cookie);\n return v ? (this.byCookie.get(v) ?? null) : null;\n }\n\n /**\n * Lookup by access token issued during the auth flow.\n *\n * @param token Bearer token.\n */\n getByToken(token: string): ClientRecord | null {\n const record = this.byToken.get(token);\n if (!record) {\n return null;\n }\n // v1.36.0 (S5): reject (and drop) an expired access token so the advertised\n // 30-min TTL is enforced \u2014 a captured token stops working once it expires.\n // Refresh tokens stay long-lived (byRefreshToken) for HA Companion compat.\n if (record.tokenExpiresAt != null && Date.now() > record.tokenExpiresAt) {\n this.byToken.delete(token);\n if (record.token === token) {\n record.token = null;\n }\n return null;\n }\n return record;\n }\n\n /**\n * Lookup by refresh token issued during the auth flow.\n *\n * @param refreshToken Refresh token value.\n */\n getByRefreshToken(refreshToken: string): ClientRecord | null {\n return this.byRefreshToken.get(refreshToken) ?? null;\n }\n\n /** Returns a snapshot array of all registered clients. */\n listAll(): ClientRecord[] {\n return [...this.byId.values()];\n }\n\n /**\n * Updates in-memory token and persists to channel.native. Old token is freed.\n *\n * @param id Client id.\n * @param token New bearer token, or null to clear.\n */\n async setToken(id: string, token: string | null): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n if (record.token) {\n this.byToken.delete(record.token);\n }\n record.token = token;\n // v1.36.0 (S5): stamp the access-token expiry so the advertised 30-min TTL is\n // actually enforced (see getByToken) \u2014 a captured token can't be replayed forever.\n record.tokenExpiresAt = token ? Date.now() + OAUTH_ACCESS_TOKEN_TTL_S * 1000 : null;\n if (token) {\n this.byToken.set(token, record);\n }\n await this.adapter.extendObjectAsync(`clients.${id}`, {\n native: { token, tokenExpiresAt: record.tokenExpiresAt },\n });\n }\n\n /**\n * Updates in-memory refresh token and persists to channel.native. Old refresh\n * token is freed. Stored plain-text in `clients.<id>.native.refreshToken` \u2014\n * same exposure profile as the access token (see {@link ClientRecord.refreshToken}).\n *\n * @param id Client id.\n * @param refreshToken New refresh token, or null to clear.\n */\n async setRefreshToken(id: string, refreshToken: string | null): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n if (record.refreshToken) {\n this.byRefreshToken.delete(record.refreshToken);\n }\n record.refreshToken = refreshToken;\n if (refreshToken) {\n this.byRefreshToken.set(refreshToken, record);\n }\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { refreshToken } });\n }\n\n /**\n * Accept an external mode write on `clients.<id>.mode`.\n *\n * Allowed values: `'global'`, `'manual'`, or any URL that passes\n * {@link coerceSafeUrl}. Empty string clears the choice \u2192 setup page.\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleModeWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n // v1.23.0 (F2): zentralisierte Validierung via parseModeWrite. Vorher\n // hatten client-registry und global-config ~80% identische Logik\n // (no-choice, non-string, sentinel, URL-coerce) dupliziert.\n const result = parseModeWrite(rawValue, [MODE_GLOBAL, MODE_MANUAL]);\n switch (result.kind) {\n case \"no-choice\":\n record.mode = \"\";\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });\n this.adapter.log.debug(`Client ${id}: mode \u2192 cleared (no-choice)`);\n return;\n case \"rejected-non-string\":\n // v1.18.0 (G7): debug statt warn \u2014 nicht-string mode-Schreibungen\n // sind UI-Echo, kein Server-Concern.\n this.adapter.log.debug(`client-registry: rejected non-string mode for ${id}`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n return;\n case \"sentinel\":\n if (result.value === MODE_MANUAL && !record.manualUrl) {\n this.adapter.log.warn(\n `Client ${id}: mode set to \"manual\" but manualUrl is empty \u2014 fill clients.${id}.manualUrl to redirect`,\n );\n }\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n this.adapter.log.debug(`Client ${id}: mode \u2192 '${result.value}' (sentinel)`);\n return;\n case \"rejected-unsafe-url\":\n this.adapter.log.warn(`Client ${id}: rejected unsafe mode value \"${result.raw}\"`);\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode, ack: true });\n return;\n case \"url\":\n record.mode = result.value;\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: result.value, ack: true });\n this.adapter.log.debug(`Client ${id}: mode \u2192 ${result.value} (direct URL)`);\n return;\n // 'rejected-disallowed-sentinel' kommt hier nicht vor weil beide\n // Sentinels (global/manual) erlaubt sind. Defensive: revert.\n default:\n await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });\n }\n }\n\n /**\n * Accept an external manualUrl write on `clients.<id>.manualUrl`.\n * Free-text \u2014 must pass {@link coerceSafeUrl} or be empty (clears).\n *\n * @param id Client id.\n * @param rawValue Value written to the state.\n */\n async handleManualUrlWrite(id: string, rawValue: unknown): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n const result = parseManualUrlWrite(rawValue);\n if (!result.ok) {\n this.adapter.log.warn(`Client ${id}: rejected unsafe manualUrl value`);\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: record.manualUrl ?? \"\", ack: true });\n return;\n }\n record.manualUrl = result.safe;\n await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: result.safe ?? \"\", ack: true });\n this.adapter.log.debug(`Client ${id}: manualUrl \u2192 ${result.safe ?? \"cleared\"}`);\n if (record.mode === MODE_MANUAL && !result.safe) {\n this.adapter.log.warn(`Client ${id}: manualUrl cleared while mode is \"manual\" \u2014 display will see the setup page`);\n }\n }\n\n /**\n * Set every client's `mode` to the same value. Used by the master switch\n * (`global.enabled`) to bulk-sync all displays \u2014 `'global'` when on,\n * `'0'` (no-choice \u2192 landing page) when off.\n *\n * Skips clients whose mode already matches (no spurious state writes).\n *\n * @param value New mode value (sentinel or URL).\n */\n async bulkSetMode(value: string): Promise<void> {\n // v1.8.1 (D7): parallele setStateAsync statt sequenziell. Mit 50 Displays\n // war das vorher 50 Broker-Round-Trips. setStateAsync ist Broker-internal,\n // Parallelism ist safe.\n const writes: Array<Promise<unknown>> = [];\n let changed = 0;\n for (const record of this.byId.values()) {\n if (record.mode === value) {\n continue;\n }\n record.mode = value;\n writes.push(this.adapter.setStateAsync(`clients.${record.id}.mode`, { val: value, ack: true }));\n changed++;\n }\n if (writes.length > 0) {\n await Promise.all(writes);\n }\n if (changed > 0) {\n this.adapter.log.debug(`bulkSetMode applied to ${changed} client(s)`);\n }\n }\n\n /**\n * Removes the client entirely \u2014 channel + states deleted, next visit creates a new entry.\n *\n * @param id Client id to forget.\n */\n async remove(id: string): Promise<void> {\n const record = this.byId.get(id);\n if (!record) {\n return;\n }\n this.byId.delete(id);\n this.byCookie.delete(record.cookie);\n if (record.token) {\n this.byToken.delete(record.token);\n }\n if (record.refreshToken) {\n this.byRefreshToken.delete(record.refreshToken);\n }\n // v1.8.1 (D2): lastSeenFlushedAt war fr\u00FCher nicht aufger\u00E4umt \u2014 bei\n // ID-Reuse (16M-Space, m\u00F6glich nach 100+ Clients \u00FCber Jahre) h\u00E4tte\n // die alte Throttle-Entry den ersten lastSeen-Write des neuen Clients\n // inhibiert. Plus minimal Memory-Leak.\n this.lastSeenFlushedAt.delete(id);\n try {\n await this.adapter.delObjectAsync(`clients.${id}`, { recursive: true });\n } catch (err) {\n // Stack-trace level \u2014 Maintainer-Diagnose, EN bleibt.\n this.adapter.log.debug(`client-registry: delObject failed for ${id}: ${String(err)}`);\n }\n this.adapter.log.info(`Client forgotten: ${id}`);\n }\n\n /**\n * Updates the mode dropdown states (`common.states`) on every client's mode datapoint.\n * Adds the `'global'` and `'manual'` sentinels on top of the discovered URLs.\n *\n * @param states Discovered URL \u2192 label map.\n */\n async syncUrlDropdown(states: UrlStates): Promise<void> {\n this.currentUrlStates = states;\n const merged = this.buildModeStates();\n // v1.27.2: extendObjectAsync mergt `common.states` tief \u2014 alte\n // URL-Schl\u00FCssel die nicht mehr discovered werden, blieben sonst im\n // Dropdown stehen (sichtbar nach v1.26\u2192v1.27 URL-Format-Wechsel:\n // alte `vis-2.0/main/index.html`-Keys neben neuen `vis-2/index.html?main`).\n // Object lesen, common.states komplett ersetzen, dann setObjectAsync.\n // v1.30.0 (R4): pro-Client get+set parallel statt sequentiell. Analog\n // gcStaleClients in main.ts (v1.28.3 M5). Sp\u00FCrbar bei Display-Farmen\n // mit 30+ Clients \u2014 vorher 2\u00D7N Broker-Round-Trips sequenziell.\n await Promise.all(\n Array.from(this.byId.keys()).map(async id => {\n const stateId = `clients.${id}.mode`;\n const existing = await this.adapter.getObjectAsync(stateId);\n if (!existing) {\n return;\n }\n existing.common.states = merged;\n await this.adapter.setObjectAsync(stateId, existing);\n }),\n );\n }\n\n // --- internal ---\n\n private trackInMemory(record: ClientRecord): void {\n this.byId.set(record.id, record);\n this.byCookie.set(record.cookie, record);\n if (record.token) {\n this.byToken.set(record.token, record);\n }\n if (record.refreshToken) {\n this.byRefreshToken.set(record.refreshToken, record);\n }\n }\n\n private async createClient(ip: string | null, hostname: string | null): Promise<ClientRecord> {\n let id = generateClientId();\n while (this.byId.has(id)) {\n id = generateClientId();\n }\n const cookie = crypto.randomUUID();\n const mode = this.newClientModeProvider();\n const record: ClientRecord = {\n id,\n cookie,\n token: null,\n refreshToken: null,\n mode,\n manualUrl: null,\n ip,\n hostname,\n };\n this.trackInMemory(record);\n await this.createObjects(record);\n this.touchLastSeen(record);\n this.adapter.log.info(\n ip ? `New client connected: ${id} (${oneLine(hostname ?? ip)})` : `New client connected: ${id}`,\n );\n // v1.19.0 (G5): IP-Burst-Detection f\u00FCr broken-cookie-Displays. Wenn\n // dieselbe IP > 3 neue Clients in einer Stunde erzeugt, ist der Cookie-\n // Mechanismus auf dem Display kaputt (aggressive Privacy, refresh-bug).\n // Einmaliger warn-Hinweis pro IP pro Stunde \u2014 danach bleibt der info-\n // log normal, aber der Operator hat wenigstens einen Anker zur Diagnose.\n if (ip) {\n this.recordNewClientIp(ip);\n }\n return record;\n }\n\n /**\n * True when `ip` has already minted {@link NEW_CLIENT_THROTTLE_PER_HOUR} new\n * clients in the current rolling hour \u2014 {@link recordNewClientIp} owns the\n * per-IP burst window. Once true, `identifyOrCreate` hands out transient\n * records (no object) until the window resets one hour after the burst began.\n *\n * @param ip Remote IP to check.\n */\n private isIpThrottled(ip: string): boolean {\n const entry = this.newClientBurst.get(ip);\n if (!entry || Date.now() - entry.since > 60 * 60 * 1000) {\n return false;\n }\n return entry.count >= NEW_CLIENT_THROTTLE_PER_HOUR;\n }\n\n /**\n * A non-persisted, untracked client handed out when an IP is over the\n * new-client throttle. No `clients.<id>` object is created and the record is\n * not added to any lookup map, so a cookieless spray cannot grow the object\n * DB. Mode is the normal new-client default, so a legitimate-but-throttled\n * client (e.g. behind a busy NAT) still resolves to the configured dashboard\n * \u2014 it just doesn't get a persistent identity.\n *\n * @param ip Remote IP (advisory).\n * @param hostname Reverse-DNS hostname, if any.\n */\n private transientRecord(ip: string | null, hostname: string | null): ClientRecord {\n return {\n id: generateClientId(),\n cookie: crypto.randomUUID(),\n token: null,\n refreshToken: null,\n mode: this.newClientModeProvider(),\n manualUrl: null,\n ip,\n hostname,\n };\n }\n\n /**\n * v1.19.0 (G5): tracking-only \u2014 wenn eine IP > 3 neue Clients pro Stunde\n * erzeugt, einmaliger warn-log mit Diagnose-Hinweis. Danach 1h cooldown\n * pro IP. Map-Cap 200 (FIFO).\n *\n * @param ip Remote IP that just got a new ClientRecord assigned.\n */\n private recordNewClientIp(ip: string): void {\n const now = Date.now();\n const HOUR = 60 * 60 * 1000;\n const entry = this.newClientBurst.get(ip) ?? { count: 0, since: now, warnedAt: 0 };\n if (now - entry.since > HOUR) {\n // Window expired \u2014 reset.\n entry.count = 0;\n entry.since = now;\n }\n entry.count += 1;\n if (entry.count > 3 && now - entry.warnedAt > HOUR) {\n this.adapter.log.warn(\n `IP ${ip} created ${entry.count} clients within an hour \u2014 display likely is not persisting cookies (privacy mode? refresh bug?)`,\n );\n entry.warnedAt = now;\n }\n this.newClientBurst.set(ip, entry);\n // v1.32.0: Soft-Cap via shared `evictOldest` (vorher inline single-shot).\n evictOldest(this.newClientBurst, NEW_CLIENT_BURST_CAP);\n }\n\n /**\n * Updates `native.lastSeen` on the channel, throttled to once per hour per\n * client. Used for the stale-client-GC: clients without token + lastSeen\n * older than 30 days get auto-removed on adapter start.\n *\n * Fire-and-forget \u2014 failures only debug-logged.\n *\n * @param record Client whose lastSeen-timestamp should be refreshed.\n */\n private touchLastSeen(record: ClientRecord): void {\n const now = Date.now();\n const last = this.lastSeenFlushedAt.get(record.id) ?? 0;\n if (now - last < 60 * 60 * 1000) {\n return; // throttle: 1\u00D7 per hour\n }\n this.lastSeenFlushedAt.set(record.id, now);\n this.adapter\n .extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } })\n .catch(err => this.adapter.log.debug(`touchLastSeen failed for ${record.id}: ${String(err)}`));\n }\n\n /**\n * v1.19.0 (F11): zentraler lastSeen-Seed-Pfad. Vorher hatte main.ts\n * gcStaleClients seinen eigenen extendObjectAsync-Call mit identischem\n * native-Format \u2014 DRY-Violation und gef\u00E4hrlich wenn das Format mal \u00E4ndert.\n * Jetzt nutzen beide Pfade diese Methode. Throttle-Map wird auch upgedated,\n * damit der n\u00E4chste touchLastSeen den seed nicht direkt \u00FCberschreibt.\n *\n * @param id Client id (short segment, ohne `clients.`-Prefix).\n * @param now Optionaler Timestamp f\u00FCr tests; default Date.now().\n */\n async seedLastSeen(id: string, now: number = Date.now()): Promise<void> {\n this.lastSeenFlushedAt.set(id, now);\n try {\n await this.adapter.extendObjectAsync(`clients.${id}`, { native: { lastSeen: now } });\n } catch (err) {\n this.adapter.log.debug(`seedLastSeen failed for ${id}: ${String(err)}`);\n }\n }\n\n /**\n * Builds the dropdown-states map for `clients.<id>.mode`. Includes the\n * `0='---'` no-choice fallback (analogous to the govee-smart pattern), the\n * `'global'` + `'manual'` sentinels, and all currently discovered URLs.\n */\n private buildModeStates(): UrlStates {\n // v1.20.0 (F4): Helper aus coerce.ts. Vorher dupliziert mit global-config\n // (bis auf den zus\u00E4tzlichen `global`-Sentinel hier, weil clients per\n // `mode='global'` an global delegieren k\u00F6nnen \u2014 global selbst nicht).\n // v1.28.4: Sentinel-Labels als plain-string in adapter.systemLanguage \u2014\n // Admin rendert common.states-VALUES direkt als React-child und crasht\n // bei Translation-Objects mit React Error #31. v1.28.0 hatte einen\n // helper hier via `as unknown as string`-Cast eingeschleust, der den\n // Type-Check still lie\u00DF aber den Admin-Dropdown beim \u00D6ffnen zerlegt\n // hat. Memory `reference_common_states_plain_string_only`.\n return buildDropdownStates(\n {\n [MODE_GLOBAL]: resolveLabel(\"globalUrl\"),\n [MODE_MANUAL]: resolveLabel(\"manualUrl\"),\n },\n this.currentUrlStates,\n );\n }\n\n /**\n * Idempotently creates all per-client objects (channel + states). Safe to\n * call repeatedly \u2014 uses `setObjectNotExistsAsync` everywhere. Called from\n * both `restore()` (so legacy v1.1.x clients gain the new mode/manualUrl\n * objects before migration writes states) and `createClient()`.\n *\n * @param record Client to create or ensure objects for.\n */\n private async ensureObjects(record: ClientRecord): Promise<void> {\n const { id, cookie, ip, hostname } = record;\n const mergedStates = this.buildModeStates();\n\n // Channel: setObjectNotExistsAsync \u2014 common.name is updated dynamically\n // by updateIpHostname() when reverse-DNS resolves; we must not clobber it.\n await this.adapter.setObjectNotExistsAsync(`clients.${id}`, {\n type: \"channel\",\n common: { name: hostname ?? ip ?? id },\n native: { cookie, token: null },\n });\n\n // States:\n // - `clients.<id>.mode` uses get+setObjectAsync (analog `global-config.ts`\n // v1.27.2-Fix): extendObjectAsync deep-merges common.states, weshalb\n // alte i18n-Object-Keys aus pre-v1.28.4-Installs unter den gleichen\n // keys h\u00E4ngen blieben \u2192 React Error #31 beim Admin-Dropdown-Open.\n // setObjectAsync ersetzt common komplett (custom-Custom-Subscriptions\n // wie influxdb.0 bleiben via spread aus existing erhalten).\n // - Repair-Pfad f\u00FCr partial-formed Objects (v1.2.0-Migration-Bug, common\n // ohne top-level type/name/role) ist auch abgedeckt: existing-fields\n // werden von der full schema common komplett \u00FCberschrieben.\n // - `clients.<id>.manualUrl` bleibt extendObjectAsync \u2014 kein states-Feld,\n // daher kein i18n-Object-Risiko.\n const modeFullCommon: ioBroker.StateCommon = {\n // tName returns StringOrTranslated, which common.name accepts directly.\n name: tName(\"clientMode\"),\n // 'mixed' future-proofs against the upcoming js-controller\n // strict-type cast (see govee-smart v1.11.0 pattern).\n type: \"mixed\",\n role: \"state\",\n read: true,\n write: true,\n def: 0,\n states: mergedStates,\n };\n const ensureModeObject = async (): Promise<void> => {\n const existing = await this.adapter.getObjectAsync(`clients.${id}.mode`);\n if (existing) {\n const preservedName = existing.common?.name;\n existing.common = { ...existing.common, ...modeFullCommon };\n if (preservedName !== undefined) {\n existing.common.name = preservedName;\n }\n existing.type = \"state\";\n await this.adapter.setObjectAsync(`clients.${id}.mode`, existing);\n } else {\n await this.adapter.setObjectAsync(`clients.${id}.mode`, {\n type: \"state\",\n common: modeFullCommon,\n native: {},\n });\n }\n };\n await Promise.all([\n ensureModeObject(),\n this.adapter.extendObjectAsync(\n `clients.${id}.manualUrl`,\n {\n type: \"state\",\n common: {\n name: tName(\"clientManualUrl\"),\n type: \"string\",\n role: \"url\",\n read: true,\n write: true,\n def: \"\",\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n ),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.ip`, {\n type: \"state\",\n common: {\n name: tName(\"clientIp\"),\n type: \"string\",\n role: \"info.ip\",\n read: true,\n write: false,\n def: \"\",\n },\n native: {},\n }),\n this.adapter.setObjectNotExistsAsync(`clients.${id}.remove`, {\n type: \"state\",\n common: {\n name: tName(\"clientRemove\"),\n type: \"boolean\",\n role: \"button\",\n read: false,\n write: true,\n def: false,\n },\n native: {},\n }),\n ]);\n }\n\n private async createObjects(record: ClientRecord): Promise<void> {\n await this.ensureObjects(record);\n const { id, mode, ip } = record;\n await Promise.all([\n this.adapter.setStateAsync(`clients.${id}.ip`, { val: ip ?? \"\", ack: true }),\n this.adapter.setStateAsync(`clients.${id}.mode`, { val: mode, ack: true }),\n this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: \"\", ack: true }),\n ]);\n }\n\n private async updateIpHostname(record: ClientRecord, ip: string | null, hostname: string | null): Promise<void> {\n if (ip && ip !== record.ip) {\n const previousIp = record.ip;\n record.ip = ip;\n await this.adapter.setStateAsync(`clients.${record.id}.ip`, { val: ip, ack: true });\n // If no hostname is known, common.name falls back to the IP. Only refresh it\n // when the channel name is STILL the old auto-IP \u2014 never clobber a name the\n // user set in the admin UI (the README documents the channel name as the\n // user-owned display label). onObjectChange does not observe clients.* renames,\n // so record.hostname stays null and this read-before-overwrite is the only\n // guard protecting a user rename across an IP change. v1.36.0 (C4).\n if (!record.hostname) {\n const obj = await this.adapter.getObjectAsync(`clients.${record.id}`);\n const currentName = obj?.common?.name;\n if (currentName === undefined || (typeof currentName === \"string\" && currentName === previousIp)) {\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: ip } });\n }\n }\n }\n if (hostname && hostname !== record.hostname) {\n record.hostname = hostname;\n await this.adapter.extendObjectAsync(`clients.${record.id}`, { common: { name: hostname } });\n }\n }\n\n private async readState(subId: string): Promise<unknown> {\n // v1.20.0 (F10): Helper aus coerce.ts \u2014 vorher dupliziert mit\n // global-config.safeGetState (gleicher try/catch-+-null-Fallback,\n // nur Pfad-Prefix anders).\n const s = await safeGetState(this.adapter, `clients.${subId}`);\n return s?.val ?? null;\n }\n}\n\n/**\n * Check whether a full state ID matches a client control datapoint and extract id + kind.\n *\n * @param fullId The full state id from a state change event.\n * @param namespace The adapter namespace (e.g. `hassemu.0`).\n */\nexport function parseClientStateId(\n fullId: string,\n namespace: string,\n): { id: string; kind: \"mode\" | \"manualUrl\" | \"remove\" } | null {\n // v1.20.0 (F9): generischer parseAdapterStateId-Helper. Vorher hatte\n // client-registry seine eigene Prefix-+-Tail-Validierung dupliziert mit\n // global-config.parseGlobalStateId.\n const parts = parseAdapterStateId(fullId, namespace, CLIENTS_PREFIX, 2);\n if (!parts) {\n return null;\n }\n const [id, kind] = parts;\n // v1.9.0 (E5): empty id rejection (`clients..mode` would parse to id='').\n if (!id) {\n return null;\n }\n if (kind !== \"mode\" && kind !== \"manualUrl\" && kind !== \"remove\") {\n return null;\n }\n return { id, kind };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["crypto", "_a"]
7
7
  }