{t('page_title')}
-{html.escape(count)}
{t('callback')} {html.escape(callback)}
-{t('pair_help')}
diff --git a/CHANGELOG.md b/CHANGELOG.md index 57bb1af..f17eda0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2026.10.6.1 — 2026-10-06 + +- Restyle the OAuth pairing page with the connector logo, a responsive card and automatic light/dark themes. +- Permit only the exact static stylesheet using a SHA-256 CSP hash; scripts and external assets remain blocked. +- Preserve all five languages, HTML escaping, count-only anonymous disclosure and the existing pairing flow. +- Add regressions that independently hash the served stylesheet in every language and check the inline logo for active content or external references. + +Updating preserves configured permissions and OAuth grants. This release passed 91 Python tests, 5 JavaScript tests and package checks. See the [focused security review](docs/SECURITY-ASSESSMENT-2026-10-06.md); no live HA deployment or Grok/Tesla acceptance test was performed by the maintainer for this release. + ## 2026.10.5.2 — 2026-10-05 - Expose action tools with readable ASCII names derived from their configured names, such as `otevrit_branu`, while retaining their original titles and descriptions. diff --git a/README.md b/README.md index a797098..6296a39 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This is an independent integration, not the official Home Assistant MCP Server o Install the published release through HACS: 1. In HACS, open **Custom repositories** and add `https://github.com/Jenik5/GrokConnectorForHomeAssistant` as an **Integration**. -2. Download **2026.10.5.2** or a later stable release. Stable releases do not require beta versions to be enabled in HACS. +2. Download **2026.10.6.1** or a later stable release. Stable releases do not require beta versions to be enabled in HACS. 3. Restart Home Assistant to load the newly installed Python integration. 4. Open **Settings → Devices & services → Add integration → Grok Connector**. 5. Enter your public HTTPS origin, for example `https://your-instance.ui.nabu.casa` or `https://ha.example.org:8125`, and choose the MCP language. Enter only the origin, without credentials, a path, query or fragment. @@ -146,3 +146,7 @@ python tools/check_package.py Tests use simulated states/actions and adapters for HA/aiohttp. They never connect to a house or control devices. GitHub workflows additionally run hassfest and HACS validation after publication. [Release procedure](docs/RELEASING.md) · [MIT license](LICENSE). Home Assistant and Grok names/marks belong to their respective owners. No endorsement or affiliation is implied. + +## Pairing page appearance + +From **2026.10.6.1**, the OAuth pairing page displays the connector logo and follows the browser's light/dark preference in a responsive layout. The pairing process and all five translations are unchanged. Only the fixed stylesheet is allowed by its CSP hash; no scripts or external assets are introduced. See the [focused security review](docs/SECURITY-ASSESSMENT-2026-10-06.md). diff --git a/custom_components/grok_connector/const.py b/custom_components/grok_connector/const.py index 8eca377..aaf1692 100644 --- a/custom_components/grok_connector/const.py +++ b/custom_components/grok_connector/const.py @@ -1,6 +1,6 @@ """Independent paths and bounds of the community connector.""" DOMAIN = 'grok_connector' -VERSION = '2026.10.5.2' +VERSION = '2026.10.6.1' MCP_PATH = '/api/grok_connector/mcp' OAUTH_PATH = '/api/grok_connector/oauth' RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource' + MCP_PATH diff --git a/custom_components/grok_connector/http.py b/custom_components/grok_connector/http.py index 993a082..e183866 100644 --- a/custom_components/grok_connector/http.py +++ b/custom_components/grok_connector/http.py @@ -16,7 +16,7 @@ from .gateway import PROTOCOLS from .diagnostics import category, emit from .security import OAuthError, allowed_callback, digest -from .oauth_page import render_authorization, transaction_cookie_name +from .oauth_page import STYLE_SOURCE, render_authorization, transaction_cookie_name from .i18n import browser_language, text NO_CACHE = {"Cache-Control": "no-store", "Pragma": "no-cache", "X-Content-Type-Options": "nosniff"} @@ -41,7 +41,7 @@ def form_headers_for_callback(callback): raise OAuthError("invalid_client") origin = "https://" + host + (":443" if parsed.port == 443 else "") return {**FORM_HEADERS, "Content-Security-Policy": - "default-src 'none'; form-action 'self' " + origin + + "default-src 'none'; style-src " + STYLE_SOURCE + "; form-action 'self' " + origin + "; base-uri 'none'; frame-ancestors 'none'"} diff --git a/custom_components/grok_connector/manifest.json b/custom_components/grok_connector/manifest.json index 57389c3..003ceab 100644 --- a/custom_components/grok_connector/manifest.json +++ b/custom_components/grok_connector/manifest.json @@ -18,5 +18,5 @@ "issue_tracker": "https://github.com/Jenik5/GrokConnectorForHomeAssistant/issues", "requirements": [], "single_config_entry": true, - "version": "2026.10.5.2" + "version": "2026.10.6.1" } diff --git a/custom_components/grok_connector/oauth_page.py b/custom_components/grok_connector/oauth_page.py index f8efd58..263ba54 100644 --- a/custom_components/grok_connector/oauth_page.py +++ b/custom_components/grok_connector/oauth_page.py @@ -1,11 +1,38 @@ +# custom_components/grok_connector/oauth_page.py """No-script localized consent document with per-transaction secure cookies.""" +import base64 import hashlib import html import re +from pathlib import Path from .const import OAUTH_PATH from .i18n import browser_language, text +# Brand icon inlined: the page CSP forbids scripts and external images, inline SVG needs neither. +LOGO = (Path(__file__).parent / 'brand/icon.svg').read_text(encoding='utf-8') +# Light by default, dark via the browser preference; HA blue is the accent colour. +STYLE = ''' +:root{color-scheme:light dark;--bg:#f4f6f8;--card:#fff;--fg:#1c2733;--muted:#5f6b7a;--line:#dbe1e8;--accent:#03a9f4;--accent-fg:#fff;--soft:#e8f6fd} +@media(prefers-color-scheme:dark){:root{--bg:#111518;--card:#1c2228;--fg:#e6ebf0;--muted:#9aa6b4;--line:#333d47;--accent:#18bcf2;--accent-fg:#06202b;--soft:#15303d}} +*{box-sizing:border-box} +body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px;background:var(--bg);color:var(--fg);font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif} +main{width:100%;max-width:440px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:32px 28px;box-shadow:0 4px 24px rgba(0,0,0,.08)} +.logo{text-align:center}.logo svg{width:84px;height:84px} +h1{font-size:1.35rem;line-height:1.3;text-align:center;margin:12px 0 16px} +.badge{background:var(--soft);border-radius:10px;padding:10px 14px;font-size:.92rem;margin:0 0 14px} +.muted{color:var(--muted);font-size:.9rem;margin:0 0 14px} +.muted strong{display:block;color:var(--fg);font:.85rem ui-monospace,monospace;word-break:break-all;margin-top:2px} +label{display:block;font-weight:600;margin:18px 0 6px} +input{width:100%;padding:12px 14px;font:1.1rem ui-monospace,monospace;letter-spacing:.06em;color:var(--fg);background:var(--bg);border:2px solid var(--line);border-radius:10px} +input:focus{outline:none;border-color:var(--accent)} +button{width:100%;margin-top:16px;padding:13px;font-family:inherit;font-weight:600;font-size:1rem;color:var(--accent-fg);background:var(--accent);border:0;border-radius:10px;cursor:pointer} +button:hover{filter:brightness(1.08)}button:focus-visible{outline:3px solid var(--fg);outline-offset:2px} +footer{margin-top:20px;text-align:center;color:var(--muted);font-size:.8rem} +''' +# CSP source allowing exactly this stylesheet (no 'unsafe-inline'); the page CSP is otherwise default-src 'none'. +STYLE_SOURCE = "'sha256-" + base64.b64encode(hashlib.sha256(STYLE.encode()).digest()).decode() + "'" + def transaction_cookie_name(transaction): if not isinstance(transaction,str) or not re.fullmatch(r'[A-Za-z0-9_-]{43}',transaction): @@ -21,11 +48,14 @@ def t(key): read_count=len(runtime.policy.readable),control_count=len(runtime.policy.actions)) # Do not publish the home's entity IDs, names or states on an anonymous page. page = f'''
- -{html.escape(count)}
{t('callback')} {html.escape(callback)}
-{t('pair_help')}