Device security and robustness (openpanel C0): check code, pnc, lenient report

- POST /devices/pair answers a 2-character `check`, listed by /devices/pending
  and shown beside each waiting entry; five codes a minute per address, and at
  50 waiting the oldest gives way instead of a 429
- portal-minted device tokens are asked for with the device's nonce (`pnc`) and
  held against it, so unpair and delete revoke them; one without `pnc` stands
  only while the nonce is 0
- a device credential acts as the device (`device_principal`), not as the
  approving superuser, so deleting that account unpairs nothing
- the report is validated port by port: unknown enum values read as `unknown`,
  unreadable ports and sections are left out and logged, `config` may name an
  entry of `config_schemas`; 422 only for no schema 1 or no object
- devices.json is parsed once per change (cached by stat), delete drops the
  report and keeps the nonce, a busy recheck runs in the background
- scope and io only from enabled flows (start/stop tells the devices), a port
  bound twice gives one io row
- the Devices tab's saves cancel and hold back device refetches until settled
This commit is contained in:
Melvin Strobl
2026-10-16 19:26:05 +02:00
parent 13d23f6baf
commit c604cfdf3b
21 changed files with 773 additions and 119 deletions
+15
View File
@@ -239,3 +239,18 @@ def test_panels_with_io_reach_the_flows_in_one_start(tmp_path: Path) -> None:
assert [(r.port, r.message, r.config) for r in store.io("knob")] == [
("backlight", "kiosk.brightness", {"wake_s": 30})
]
def test_the_nonces_are_read_again_only_when_the_file_changes(tmp_path: Path) -> None:
"""Every device request reads its nonce, so the file is parsed once per
change, and a change from outside this process is still seen."""
store = DeviceStore(FlowStore(tmp_path / "flows"), tmp_path / "devices.json")
assert store.nonce("hall") == 0
store.bump("hall")
assert store.nonce("hall") == 1
assert store._states() is store._states()
raw = json.loads(store.state_file.read_text())
raw["hall"]["nonce"] = 7
store.state_file.write_text(json.dumps(raw, indent=4))
assert store.nonce("hall") == 7