Files
app/backend/fluksio/api/routes/connectors.py
T
stroblmeandClaude Opus 5.5 847abd67d6 Give connectors a store that survives restarts, and a way to clear it
`ConnectorNode.store` keeps short strings per connector package in the
engine's database (`connector_state`, migration f8c3e5a91d27), so the nodes of
one package share them and `fluksio serve` needs nothing new: get, set and
delete, at most 64 kB a value and 256 keys a package. A superuser lists the
keys (never the values) and clears them through `/connectors/{node_type}/state`,
or under Stored state in a connector node's panel. Documented in the connector
contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
2026-10-06 17:19:48 +02:00

80 lines
2.9 KiB
Python

"""What a connector keeps across restarts, for a superuser to see and reset.
A connector writes these itself (``ConnectorNode.store``). This is the way back
when one of them is wrong: a Hue bridge replaced, so the certificate pinned for
it no longer matches. Values are never handed out, since a connector may keep a
token here; the keys say enough to decide what to clear.
A connector is named by any node type it installs, as the canvas knows it, and
its keys are shared by every node type of the same package.
"""
from datetime import datetime
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from fluksio.api.deps import get_current_active_superuser
from fluksio.flow.connector import ConnectorNode
from fluksio.flow.connector_store import KEY, ConnectorStore, namespace_of
from fluksio.flow.controller import NODE_TYPES
from fluksio.models import Message
router = APIRouter(
prefix="/connectors",
tags=["connectors"],
dependencies=[Depends(get_current_active_superuser)],
)
class StoredKey(BaseModel):
key: str
#: Bytes, so a superuser can tell a fingerprint from a blob.
size: int
updated_at: datetime
class ConnectorStatePublic(BaseModel):
#: The package the keys belong to.
connector: str
data: list[StoredKey]
count: int
def _store(node_type: str) -> ConnectorStore:
"""The store of the connector that installed *node_type*, or a 404."""
known = NODE_TYPES.get(node_type)
cls = known.cls if known is not None else None
if not (isinstance(cls, type) and issubclass(cls, ConnectorNode)):
raise HTTPException(status_code=404, detail=f"No connector named '{node_type}'")
return ConnectorStore(namespace_of(cls))
@router.get("/{node_type}/state", response_model=ConnectorStatePublic)
def read_connector_state(node_type: str) -> Any:
"""The keys this connector keeps, without their values."""
store = _store(node_type)
keys = [
StoredKey(key=row.key, size=len(row.value.encode()), updated_at=row.updated_at)
for row in store.entries()
]
return ConnectorStatePublic(connector=store.connector, data=keys, count=len(keys))
@router.delete("/{node_type}/state", response_model=Message)
def clear_connector_state(node_type: str) -> Any:
"""Forget everything this connector keeps. It starts over on next use."""
store = _store(node_type)
cleared = store.clear()
return Message(message=f"Cleared {cleared} key(s) of {store.connector}")
@router.delete("/{node_type}/state/{key}", response_model=Message)
def delete_connector_state(node_type: str, key: str) -> Any:
"""Forget one key, such as the certificate pinned for one bridge."""
store = _store(node_type)
if not KEY.fullmatch(key) or not store.delete(key):
raise HTTPException(status_code=404, detail=f"No key '{key}' stored")
return Message(message=f"Deleted '{key}' of {store.connector}")