|
7 | 7 | HA uses: |
8 | 8 | - GET /devices (list configured + importable devices) |
9 | 9 | - GET /json-config?configuration=... (parsed YAML as JSON) |
| 10 | +- POST /encryption-key (HA-provisioned API key handoff; ingress site only) |
10 | 11 | - /compile (WebSocket, spawn protocol) |
11 | 12 | - /upload (WebSocket, spawn protocol) |
12 | 13 |
|
|
47 | 48 | TERMINAL_JOB_STATUSES, |
48 | 49 | Device, |
49 | 50 | DeviceState, |
| 51 | + ErrorCode, |
50 | 52 | EventType, |
51 | 53 | FirmwareJob, |
52 | 54 | JobType, |
@@ -337,6 +339,52 @@ async def _json_config_response(db: DeviceBuilder, configuration: str) -> web.Re |
337 | 339 | return json_response(config) |
338 | 340 |
|
339 | 341 |
|
| 342 | +def _parse_encryption_key_payload(raw: bytes) -> tuple[str, str, str] | str: |
| 343 | + """Parse the POST /encryption-key body; ``(device_name, key, mac)`` or an error message.""" |
| 344 | + try: |
| 345 | + body = loads(raw) |
| 346 | + except JSONDecodeError: |
| 347 | + return "invalid JSON body" |
| 348 | + if not isinstance(body, dict): |
| 349 | + return "invalid JSON body" |
| 350 | + device_name = body.get("device_name") |
| 351 | + key = body.get("key") |
| 352 | + mac = body.get("mac", "") |
| 353 | + if not isinstance(device_name, str) or not device_name: |
| 354 | + return "device_name is required" |
| 355 | + if not isinstance(key, str) or not key: |
| 356 | + return "key is required" |
| 357 | + if not isinstance(mac, str): |
| 358 | + return "mac must be a string" |
| 359 | + return device_name, key, mac |
| 360 | + |
| 361 | + |
| 362 | +async def _encryption_key_response(request: web.Request) -> web.Response: |
| 363 | + """ |
| 364 | + Land an HA-provisioned key via the devices controller. |
| 365 | +
|
| 366 | + Only accepted on the trusted, peer-guarded ingress site: the key is |
| 367 | + secret material and the supervisor-authenticated ingress path is the |
| 368 | + one channel where the caller is known to be Home Assistant. |
| 369 | + """ |
| 370 | + if not (request.app.get("trusted_site") and request.app.get("peer_guarded")): |
| 371 | + return json_response( |
| 372 | + {"error": "encryption-key is only accepted over the Home Assistant ingress"}, |
| 373 | + status=403, |
| 374 | + ) |
| 375 | + parsed = _parse_encryption_key_payload(await request.read()) |
| 376 | + if isinstance(parsed, str): |
| 377 | + return json_response({"error": parsed}, status=400) |
| 378 | + device_name, key, mac = parsed |
| 379 | + db = request.app["device_builder"] |
| 380 | + try: |
| 381 | + result = await db.devices.set_encryption_key(name=device_name, key=key, mac=mac) |
| 382 | + except CommandError as err: |
| 383 | + status = 400 if err.code is ErrorCode.INVALID_ARGS else 500 |
| 384 | + return json_response({"error": err.message}, status=status) |
| 385 | + return json_response(result) |
| 386 | + |
| 387 | + |
340 | 388 | def create_legacy_routes() -> web.RouteTableDef: |
341 | 389 | """Create backward-compatible REST + WS routes for HA.""" |
342 | 390 | routes = web.RouteTableDef() |
@@ -396,6 +444,11 @@ async def legacy_json_config(request: web.Request) -> web.Response: |
396 | 444 | db = request.app["device_builder"] |
397 | 445 | return await _json_config_response(db, request.query.get("configuration", "")) |
398 | 446 |
|
| 447 | + @routes.post("/encryption-key") |
| 448 | + async def legacy_encryption_key(request: web.Request) -> web.Response: |
| 449 | + """HA-provisioned Noise API key — splice into a configured YAML or stash for adoption.""" |
| 450 | + return await _encryption_key_response(request) |
| 451 | + |
399 | 452 | @routes.get("/compile") |
400 | 453 | async def legacy_compile(request: web.Request) -> web.WebSocketResponse: |
401 | 454 | return await _handle_legacy_ws_command(request, JobType.COMPILE) |
|
0 commit comments