* fix(security): mask secret fields in API responses and extract helpers
GET /config/secrets returned raw API keys in plaintext to the browser.
GET /plugins/config returned merged config including deep-merged secrets.
POST /plugins/config could overwrite existing secrets with empty strings
when the GET endpoint returned masked values that were sent back unchanged.
Changes:
- Add src/web_interface/secret_helpers.py with reusable functions:
find_secret_fields, separate_secrets, mask_secret_fields,
mask_all_secret_values, remove_empty_secrets
- GET /config/secrets: mask all values with '••••••••'
- GET /plugins/config: mask x-secret fields with ''
- POST /plugins/config: filter empty-string secrets before saving
- pages_v3: mask secrets before rendering plugin config templates
- Remove three duplicated inline find_secret_fields/separate_secrets
definitions in api_v3.py (replaced by single imported module)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden secret masking against CodeRabbit findings
- Fail-closed: return 500 when schema unavailable instead of leaking secrets
- Fix falsey masking: use `is not None and != ''` instead of truthiness check
so values like 0 or False are still redacted
- Add array-item secret support: recurse into `type: array` items schema
to detect and mask secrets like accounts[].token
- pages_v3: fail-closed when schema properties missing
Addresses CodeRabbit findings on PR #276:
- Critical: fail-closed bypass when schema_mgr/schema missing
- Major: falsey values not masked (0, False leak through)
- Major: pages_v3 fail-open when schema absent
- Major: array-item secrets unsupported
Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): remove shadowed sys import in plugin action handler
Two `import sys` statements inside execute_plugin_action() and
authenticate_spotify() shadowed the module-level import, causing
"cannot access local variable 'sys'" errors when sys.executable
was referenced in earlier branches of the same function.
Also fixes day number validation in the of-the-day upload endpoint
to accept 366 (leap year).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): correct validation message from 1-365 to 1-366
The JSON structure validation message still said '1-365' while the
actual range check accepts 1-366 for leap years. Make all three
validation messages consistent.
Addresses CodeRabbit finding on PR #280.
Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The secrets template used "weather" as the key, but the weather plugin's
ID is "ledmatrix-weather". Since ConfigManager deep-merges secrets into
the main config by key, secrets under "weather" never reached the plugin
config at config["ledmatrix-weather"], making the API key invisible to
the plugin.
Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>