Follows the portal: the noun is "instance" everywhere the app says it — UI strings, CLI output, error details, docs and comments. The wire keys (`instance_id`, `instance_token`) and the hub route this calls move with it. An existing cloud.json is adopted rather than refused: without the key alias the dataclass fails to parse, which the caller swallows and reads as "never enrolled" instead of "reconnect". `instance_key` on a node type becomes `target_key`. It means the outside thing a node points at, which is a different sense of the word, and keeping both would put two meanings of "instance" in one codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
"""Credentials for node integrations.
|
|
|
|
Values go in and are never handed back out — the API only ever lists names.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from pydantic import BaseModel
|
|
|
|
from fluksio.api.deps import get_current_active_superuser
|
|
from fluksio.flow.secrets import SecretNotFound, get_secrets
|
|
from fluksio.models import Message
|
|
|
|
# Superuser, not merely signed in. The names alone say what this instance
|
|
# talks to, and `PUT /{name}` takes any name — so an ordinary account could
|
|
# overwrite the credential a flow authenticates with. `/search` already gates
|
|
# secret names this way and said so; this router was the half that did not.
|
|
router = APIRouter(
|
|
prefix="/secrets",
|
|
tags=["secrets"],
|
|
dependencies=[Depends(get_current_active_superuser)],
|
|
)
|
|
|
|
|
|
class SecretNames(BaseModel):
|
|
data: list[str]
|
|
count: int
|
|
|
|
|
|
class SecretValue(BaseModel):
|
|
value: str
|
|
|
|
|
|
@router.get("/", response_model=SecretNames)
|
|
def read_secrets() -> Any:
|
|
"""List the names of stored secrets."""
|
|
names = get_secrets().list()
|
|
return SecretNames(data=names, count=len(names))
|
|
|
|
|
|
@router.put("/{name}", response_model=Message)
|
|
async def save_secret(name: str, body: SecretValue) -> Any:
|
|
"""Store a secret under a name that nodes can reference."""
|
|
await run_in_threadpool(get_secrets().set, name, body.value)
|
|
return Message(message=f"Saved secret '{name}'")
|
|
|
|
|
|
@router.delete("/{name}", response_model=Message)
|
|
async def delete_secret(name: str) -> Any:
|
|
"""Delete a secret."""
|
|
try:
|
|
await run_in_threadpool(get_secrets().delete, name)
|
|
except SecretNotFound:
|
|
raise HTTPException(status_code=404, detail=f"No secret named '{name}'")
|
|
return Message(message=f"Deleted secret '{name}'")
|