Repository navigation
fix(plugins): isolate plugins from HomeGlow and from each other - #320
Conversation
Reported in mrramam#50, #49 and #62 (the CORS part). Plugin frames were sandboxed with allow-same-origin and served from HomeGlow's origin, and the API allowed any origin, so a plugin could do anything the Admin Panel can: write settings (the proxy whitelist included), export the database and its key, stage a restore, restart the server, and use another plugin's storage, notifications or Home Assistant panel by naming its id. - Opaque sandbox: plugin frames (and the Admin preview) drop allow-same-origin. The dashboard posts to them with "*" and accepts their messages from origin "null", still from its own frame only. - A token per page: /widgets/:filename injects a frozen identity and a frame script (utils/pluginFrameShims.js) carrying an HMAC token for "plugin:<id>" (or "widget:<filename>"), signed with PLUGIN_TOKEN_SECRET (made on first use, redacted). The script adds it to fetch and XHR calls to HomeGlow's own host only, so plugins and the SDK need no change. - One request policy (utils/pluginRequestPolicy.js): a request with Origin: null needs a valid token, and reaches only its own /api/plugin/v1/{storage,settings,ha,notify}/<id> routes, the core reads and the four household writes the plugin guide documents, the proxy, and theme files (no token: FontFace can't add one). Everything else answers 403. calendar-sources leaves out url and username for plugins. - CORS: @fastify/cors answers per request instead of "*": a plugin's allowed requests, the dev server, and a new CORS_ORIGINS list. The proxy and the event stream no longer set "*" by hand. - localStorage: the frame script puts a per-plugin stand-in in its place (the real one throws in an opaque origin), seeded through the frame's name and kept by the dashboard per plugin and display (client/src/utils/pluginLocalStore.js). The published plugins' old keys are carried over once per display, for the plugins installed then only. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
I checked this against a bunch of plugin's API calls, plus the plugins we run in production.
It merges cleanly onto my production build, with #319 too. |
|
-Routine is built in so it would only break if someone reinstalls it and I think we can consider the plugin deprecated. I'm going to push 1.10 and this will not be merged in that release, this will go in 1.11 and can be merged to main once 1.10 goes live. |
…e 1.10 jherforth#321, plugin storage/proxy fixes jherforth#319, chore timezone display jherforth#318, CI pins jherforth#316-317, prize descriptions jherforth#315, proxmox/docker fixes jherforth#314, plugin proxy docs jherforth#313, Google OAuth escaping jherforth#312, upload typing/sandbox jherforth#311 Resolve registry conflict: keep fork renumbering (local 28 userGoogleTasks, 34 googleTasksTables); upstream's new 35 proxyWhitelistSeed and 36 prizeDescription renumbered to 41 and 42 to keep schemaIds unique and matching file names.
Since jherforth#320 a plugin frame has an opaque origin, so its fetch of /fonts/inter-*.woff2 is always CORS mode and the browser refuses it without an Access-Control-Allow-Origin header. The dashboard itself is same-origin and unaffected, which is why the test instance looked fine. - Dev backend: /fonts/ joins PUBLIC_ROUTES, so an opaque-origin GET needs no plugin token (the CORS delegator then reflects the origin, as it already does for theme fonts). - Containers: a ^~ /fonts/ location in nginx.conf serves the files with Access-Control-Allow-Origin: * ahead of the static-asset regex, which adds no CORS header. Tests: plugin policy, SDK theme, and isolation suites pass (28/28).
…e location (#323) After #320, Music, Sunrise & Sunset and WeatherGlance broke on hgtest. - The SDK never loaded. Some browsers send Origin: null with a sandboxed page's script and style loads, and the request policy answered every Origin: null request without a token with 403, /plugin-sdk/v1.js included. Without window.HomeGlow, Music threw on HG.settings, and Sunrise & Sunset and WeatherGlance couldn't read their saved locations (WeatherGlance then asked /api/weather with no location: 404). A GET or HEAD the policy refuses now goes through without CORS headers, so the page can use but not read it; no GET route changes anything. A fetch the policy refuses (Sec-Fetch-Mode: cors) still gets the readable 403, and writes still need a token and an allowed route. - Location detection was blocked by permissions policy: a cross-origin (here opaque) frame can't ask for the location unless its iframe allows it. Plugin frames, and the Admin preview, now carry allow="geolocation". The browser still asks the household first. Co-authored-by: Claude Opus 5.5 <[email protected]>
Plugin frames have opaque origins since jherforth#320, so they cannot share the dashboard's font cache and re-downloaded the 350KB Inter files on every load. The @font-face rules move out of index.css (which plugin frames link as /index.css) into fonts.css, imported by main.jsx, with the woff2 files bundled by Vite from client/src/assets/fonts/ into hashed assets the dashboard fetches once and serves from cache. Removed what only plugins needed: server/fonts/ (woff2, OFL, README), the @fastify/static /fonts/ registration, the /fonts/ PUBLIC_ROUTES entry and its test, and the nginx /fonts/ location (its CORS header existed for plugin frames; the dashboard's hashed assets are covered by the generic static-asset cache headers). Release notes call out the system-font-to-Inter change for most households.
- "Idle seconds before dimming" moves up beside "Dim the display when idle": it's when dimming starts, not part of the night hours. Under "Night hours" are just its start and end. - The "Night hours" heading gets space below it; the fields' floating labels overlapped it by 9 px. - Idle seconds is saved as a number (editWholeNumber), not the field's text. - homeglow:activity counts only from a frame on this page, like every other plugin message since jherforth#320. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* feat(appearance): configurable display dimming as an appearance setting Move the fork's kiosk dimming (CSS brightness filter, idle fade, day/night targets) from URL params + localStorage into the appearance cascade: a dimming object field with household default and per-display override, normalized and clamped like autoDark. The old localStorage keys seed a display override once via the legacy-migration pattern (with their own marker, so a theme migrated long ago does not block the dim seed). Kiosk URL params still override per-field for the session, and saving in Admin applies live without a reload. Generated with Muse AI 🤖 * docs: screenshot of the dimming settings for the upstream PR * feat(appearance): show dimming night hours as times and slider percentages live Night start/end show their formatted time under the hour field, using the app's standard time format (12-hour where that is the locale convention, 24-hour elsewhere). Brightness headings now carry the live percentage, so the value is visible while the slider moves, not only in the drag bubble. * feat(appearance): dimming gets its own card, scope-isolated saves, household timezone Address jherforth's + mrramam's review on the dimming PR: - Own card: dimming moves out of AppearanceSettings into DisplayDimmingSettings, rendered in the Look tab between Appearance and Themes as its own AdminFormSection with its own scope toggle, Use-household switch and Save button. - Scope isolation: both cards save through the new appearanceSave helper, which re-reads the stored appearance and merges only its own fields (device saves delete fields switched to Use household instead of omitting them). Saving dimming, then Appearance without reloading, leaves dimming intact, and vice versa; Make-household- default preserves every display's dimming override. - Labels: slider headings are subtitle2 with the live percentage after the label; the three number fields sit under a subtitle2 Night hours heading (new en+es string). - Night start/end are Selects of the 24 hours, labelled with formatHourOfDay; idle seconds stays a number field. - Dimming pauses while the Admin Panel is open and resumes after a full idle delay on close. - Dropped fork-only pieces: kiosk URL parameters (?dim, ?dimNight, ?dimAfter, ?nightStart, ?nightEnd), the window.hgSetDim hook, and the hg-dim/hg-dim-night/hg-dim-after legacy seeding with its dimmingMigrated marker (theme migration untouched). - Plugin frames now report pointer/key/wheel use as homeglow:activity (always on, 1/sec throttle) and displayDim counts it as activity, so using a plugin holds the dim off. - The night window is read in the household timezone (getServerTimezoneSync) via Intl, with a local-hours fallback for unknown zones. - The PR-body screenshot is deleted from the repo. No low-end-panel frame-rate measurement: the dimming filter sits on <html> and theme animations are transform/opacity-only, so it is addressed by mechanism, not measured on hardware. Generated with Muse AI 🤖 * fix(appearance): idle seconds sits with the switch, Night hours has room - "Idle seconds before dimming" moves up beside "Dim the display when idle": it's when dimming starts, not part of the night hours. Under "Night hours" are just its start and end. - The "Night hours" heading gets space below it; the fields' floating labels overlapped it by 9 px. - Idle seconds is saved as a number (editWholeNumber), not the field's text. - homeglow:activity counts only from a frame on this page, like every other plugin message since #320. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: adamecker <[email protected]> Co-authored-by: jherforth <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
Draft: don't merge yet. The release order is still being decided (1.10 first, or with this).
Fixes mrramam#50, #49 and the CORS part of #62. Until now, a plugin could do anything the Admin Panel can:
That was possible because plugin frames were sandboxed with
allow-same-originand served from HomeGlow's own origin, and the API allowed any origin.What changes
Opaque sandbox.
allow-same-origin. A plugin can't reach the dashboard's DOM or storage, or read another plugin's page."*", and accepts their messages from origin"null", still only from its own frame.A token per page.
/widgets/:filenameinjects a frozen__HOMEGLOW_PLUGIN__, plus a frame script (server/utils/pluginFrameShims.js).plugin:<id>, orwidget:<filename>for a plain widget. It's signed with a newPLUGIN_TOKEN_SECRETsetting, which is made on first use and never returned.fetchand XHR calls to HomeGlow's own host, and to nothing else. Plugins and the SDK need no change.One request policy (
server/utils/pluginRequestPolicy.js). A request withOrigin: nullneeds a valid token, and reaches only:/api/plugin/v1/{storage,settings,ha,notify}/<id>routes;FontFacecan't add one.Everything else answers
403with a reason./api/calendar-sourcesleaves outurlandusernamefor plugins, since a private ICS link is a secret.CORS.
@fastify/corsnow answers per request instead of*: a plugin's allowed requests, the dev server, and a newCORS_ORIGINSlist.*by hand.localStoragethrows in an opaque origin, so the frame script puts a stand-in in its place, per plugin and per display.nameand kept by the dashboard (client/src/utils/pluginLocalStore.js).Compatibility
403;localStoragekeys saved before the upgrade aren't carried over, only the published plugins' are.Testing
npm test:*. That test is updated and passes, and asks as a plugin frame does.pluginRequestPolicy.test.js: tokens, routes, preflights, public files, origins.pluginIsolation.test.js, on a real server with requests sent as a sandboxed frame sends them:Originrequests unchanged.vitest(749, including newpluginLocalStore.test.js),check:i18nandbuildall pass.localStorage(by method and by property), theme files, the theme message and a declared event all work.localStoragepersists across a reload.🤖 Generated with Claude Code