velxio/backend/app/services/board_access.py

54 lines
2.0 KiB
Python

"""
Pro board-access gate — server-side enforcement seam.
STM32 and the QEMU-backed Raspberry Pi boards are Pro-only on velxio.dev. The
frontend gate (picker add + run) is the UX layer; this is the server-side
enforcement for the WebSocket simulation start, because on velxio.dev the QEMU
binary is present for every session, so the frontend gate alone is bypassable.
OSS / self-hosted has no binary at all (the start fails with a Pro-framed
message regardless), so this gate is a no-op there.
The pro overlay calls ``register_board_access_gate()`` from ``register_pro()``
with an implementation that resolves the user from the WebSocket cookies and
returns False for a non-paid web user. The desktop sidecar (VELXIO_DESKTOP=1)
always allows — the Tauri license already gates the whole app. Default with no
overlay installed: allow.
This mirrors the existing ``try: from app.pro import register_pro`` extension
pattern — a generic seam any private extension could populate.
"""
from __future__ import annotations
import logging
from typing import Awaitable, Callable, Optional
logger = logging.getLogger(__name__)
# gate(websocket, board_kind) -> True to allow, False to block (Pro-gated).
BoardAccessGate = Callable[[object, str], Awaitable[bool]]
_gate: Optional[BoardAccessGate] = None
PRO_BOARD_MESSAGE = (
'STM32 and Raspberry Pi emulation are Velxio Pro features. '
'Use them on velxio.dev with a paid plan, or install Velxio Desktop.'
)
def register_board_access_gate(fn: Optional[BoardAccessGate]) -> None:
"""Install the gate (pro overlay) or clear it (None)."""
global _gate
_gate = fn
async def board_allowed(websocket: object, board_kind: str) -> bool:
"""True if this session may start the given Pro board. No gate -> allow."""
if _gate is None:
return True
try:
return await _gate(websocket, board_kind)
except Exception as exc: # fail-open: a gate bug must never wedge the sim
logger.warning('board_access gate raised, allowing: %r', exc)
return True