from __future__ import annotations import hashlib import hmac import secrets from datetime import datetime, timezone from typing import Annotated, Any from uuid import uuid4 import sqlalchemy as sa from fastapi import APIRouter, Header, HTTPException, Request, status from fastapi.routing import APIRoute from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from hub_core.runtime.compat_catalogs import ( ANNOTATION_CATEGORIES, EVENT_TYPES, POLICY_SCOPES, WIDGET_TYPES, ) from hub_core.runtime.models import EventCommand from hub_core.runtime.postgres_store import PostgresPortStore from hub_core.runtime.tables import ( compat_api_consumers, compat_api_keys, compat_hubs, compat_manifests, compat_widgets, runtime_interaction_events, ) from hub_core.utils.slugs import slugify_or_default AuthorizationHeader = Annotated[str | None, Header()] class SQLCompatibilityStore: def __init__(self, ports: PostgresPortStore) -> None: self.ports = ports self.sessions = ports.sessions async def validate_token(self, token: str) -> bool: token_hash = _hash_token(token) async with self.sessions() as session: result = await session.execute( sa.select(compat_api_keys.c.id).where( compat_api_keys.c.key_hash == token_hash, compat_api_keys.c.status == "active", ) ) return result.scalar_one_or_none() is not None async def list_hubs(self) -> list[dict[str, Any]]: async with self.sessions() as session: rows = ( await session.execute(sa.select(compat_hubs).order_by(compat_hubs.c.slug)) ).mappings() return [_hub(row) for row in rows] async def create_hub(self, body: dict[str, Any]) -> dict[str, Any]: now = _now() row = { "id": str(uuid4()), "slug": body["slug"], "name": body["name"], "domain": body.get("domain"), "hub_kind": body.get("hubKind"), "hub_family": body.get("hubFamily"), "vsm_function": body.get("vsmFunction"), "vsm_system": body.get("vsmSystem"), "status": body.get("status", "active"), "description": body.get("description"), "body": body, "created_at": now, "updated_at": now, } async with self.sessions.begin() as session: await session.execute(compat_hubs.insert().values(**row)) await self.ports._audit( session, action="compat.hub.created", subject_type="hub", subject_id=row["id"], correlation_id=None, value=body, ) return _hub(row) async def list_manifests(self, hub_id: str | None = None) -> list[dict[str, Any]]: statement = sa.select(compat_manifests).order_by(compat_manifests.c.created_at) if hub_id: statement = statement.where(compat_manifests.c.hub_id == hub_id) async with self.sessions() as session: rows = (await session.execute(statement)).mappings() return [_manifest(row) for row in rows] async def create_manifest(self, body: dict[str, Any]) -> dict[str, Any]: now = _now() hub_id = body.get("hubId") hub_slug = None if hub_id: async with self.sessions() as session: hub_slug = ( await session.execute( sa.select(compat_hubs.c.slug).where(compat_hubs.c.id == hub_id) ) ).scalar_one_or_none() row = { "id": str(uuid4()), "hub_id": hub_id, "hub_slug": hub_slug, "manifest_version": body.get("manifestVersion", "0.1.0"), "status": body.get("status", "draft"), "body": body, "created_at": now, "updated_at": now, } async with self.sessions.begin() as session: await session.execute(compat_manifests.insert().values(**row)) await self.ports._audit( session, action="compat.manifest.created", subject_type="manifest", subject_id=row["id"], correlation_id=None, value=body, ) return _manifest(row) async def update_manifest( self, manifest_id: str, body: dict[str, Any], *, activate: bool = False ) -> dict[str, Any]: async with self.sessions.begin() as session: current = ( await session.execute( sa.select(compat_manifests).where(compat_manifests.c.id == manifest_id) ) ).mappings().one_or_none() if current is None: return {"id": manifest_id, "status": "missing"} payload = dict(current["body"] or {}) payload.update(body) values = { "body": payload, "manifest_version": payload.get( "manifestVersion", current["manifest_version"] ), "status": "active" if activate else current["status"], "updated_at": _now(), } await session.execute( compat_manifests.update() .where(compat_manifests.c.id == manifest_id) .values(**values) ) await self.ports._audit( session, action="compat.manifest.activated" if activate else "compat.manifest.updated", subject_type="manifest", subject_id=manifest_id, correlation_id=None, value=payload, ) return _manifest({**dict(current), **values}) async def list_consumers(self) -> list[dict[str, Any]]: async with self.sessions() as session: rows = ( await session.execute( sa.select(compat_api_consumers).order_by(compat_api_consumers.c.name) ) ).mappings() return [_consumer(row) for row in rows] async def create_consumer(self, body: dict[str, Any]) -> dict[str, Any]: row = { "id": str(uuid4()), "slug": body.get("slug") or slugify_or_default(body["name"]), "name": body["name"], "description": body.get("description"), "hub_capability_manifest_id": body.get("hubCapabilityManifestId"), "rate_limit_per_minute": body.get("rateLimitPerMinute"), "quota_per_day": body.get("quotaPerDay"), "key_prefix": None, "status": body.get("status", "active"), "body": body, "created_at": _now(), } async with self.sessions.begin() as session: await session.execute(compat_api_consumers.insert().values(**row)) await self.ports._audit( session, action="compat.consumer.created", subject_type="api_consumer", subject_id=row["id"], correlation_id=None, value=body, ) return _consumer(row) async def create_key(self, consumer_id: str, scopes: str | None) -> dict[str, Any]: full_key = f"ch_{secrets.token_urlsafe(32)}" prefix = full_key[:12] row = { "id": str(uuid4()), "api_consumer_id": consumer_id, "key_prefix": prefix, "key_hash": _hash_token(full_key), "scopes": scopes, "status": "active", "created_at": _now(), } async with self.sessions.begin() as session: await session.execute(compat_api_keys.insert().values(**row)) await session.execute( compat_api_consumers.update() .where(compat_api_consumers.c.id == consumer_id) .values(key_prefix=prefix) ) await self.ports._audit( session, action="compat.api_key.created", subject_type="api_key", subject_id=row["id"], correlation_id=None, value={"apiConsumerId": consumer_id, "keyPrefix": prefix, "scopes": scopes}, ) return { "fullKey": full_key, "apiKey": { "id": row["id"], "apiConsumerId": consumer_id, "keyPrefix": prefix, "scopes": scopes, "status": "active", }, } async def list_widgets(self) -> list[dict[str, Any]]: async with self.sessions() as session: rows = ( await session.execute(sa.select(compat_widgets).order_by(compat_widgets.c.name)) ).mappings() return [_widget(row) for row in rows] async def create_widget(self, body: dict[str, Any]) -> dict[str, Any]: row = { "id": str(uuid4()), "hub_id": body["hubId"], "name": body["name"], "widget_type": body.get("widgetType"), "capability_ref": body.get("capabilityRef"), "view_context": body.get("viewContext"), "policy_scope": body.get("policyScope"), "status": body.get("status", "active"), "body": body, "created_at": _now(), } async with self.sessions.begin() as session: await session.execute(compat_widgets.insert().values(**row)) await self.ports._audit( session, action="compat.widget.created", subject_type="widget", subject_id=row["id"], correlation_id=None, value=body, ) return _widget(row) async def list_interactions(self) -> list[dict[str, Any]]: async with self.sessions() as session: rows = ( await session.execute( sa.select(runtime_interaction_events).order_by( runtime_interaction_events.c.recorded_at, runtime_interaction_events.c.id, ) ) ).mappings() values: list[dict[str, Any]] = [] for row in rows: legacy = dict((row["payload"] or {}).get("legacy") or {}) if legacy: legacy["id"] = row["id"] values.append(legacy) return values async def create_interaction(self, body: dict[str, Any]) -> dict[str, Any]: correlation_id = uuid4() command = EventCommand( schema_version="0.1.0", correlation_id=correlation_id, event_type=body["eventType"], occurred_at=_now(), subject_refs={"widget": body["widgetId"]}, payload={"legacy": body}, ) accepted = await self.ports.append_interaction(command) return {"id": accepted.id, **body} def create_compatibility_router() -> APIRouter: router = APIRouter() @router.get("/api/v2/widget-types") async def widget_types(request: Request) -> list[dict[str, str]]: _enabled(request, "system") return WIDGET_TYPES @router.get("/api/v2/event-types") async def event_types(request: Request) -> list[dict[str, str]]: _enabled(request, "system") return EVENT_TYPES @router.get("/api/v2/annotation-categories") async def annotation_categories(request: Request) -> list[dict[str, str]]: _enabled(request, "system") return ANNOTATION_CATEGORIES @router.get("/api/v2/policy-scopes") async def policy_scopes(request: Request) -> list[dict[str, str]]: _enabled(request, "system") return POLICY_SCOPES @router.get("/api/v2/hubs") async def list_hubs(request: Request, authorization: AuthorizationHeader = None) -> dict: await _protected(request, "registry", authorization) return _page(await _store(request).list_hubs()) @router.post("/api/v2/hubs", status_code=status.HTTP_201_CREATED) async def create_hub( body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "registry", authorization, write=True) return await _store(request).create_hub(body) @router.get("/api/v2/hub-capability-manifests") async def list_manifests( request: Request, authorization: AuthorizationHeader = None, hubId: str | None = None, ) -> dict: await _protected(request, "registry", authorization) return _page(await _store(request).list_manifests(hubId)) @router.post("/api/v2/hub-capability-manifests", status_code=status.HTTP_201_CREATED) async def create_manifest( body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "registry", authorization, write=True) return await _store(request).create_manifest(body) @router.patch("/api/v2/hub-capability-manifests/{manifest_id}") async def patch_manifest( manifest_id: str, body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None, ) -> dict: await _protected(request, "registry", authorization, write=True) return await _store(request).update_manifest(manifest_id, body) @router.post("/api/v2/hub-capability-manifests/{manifest_id}/activate") async def activate_manifest( manifest_id: str, request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "registry", authorization, write=True) return await _store(request).update_manifest(manifest_id, {}, activate=True) @router.get("/api/v2/hub-registry") async def hub_registry( request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "registry", authorization) store = _store(request) return { "data": { "hubs": await store.list_hubs(), "hubCapabilityManifests": await store.list_manifests(), } } @router.get("/api/v2/api-consumers") async def list_consumers( request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "credentials", authorization) return _page(await _store(request).list_consumers()) @router.post("/api/v2/api-consumers", status_code=status.HTTP_201_CREATED) async def create_consumer( body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "credentials", authorization, write=True) return await _store(request).create_consumer(body) @router.post( "/api/v2/api-consumers/{consumer_id}/api-keys", status_code=status.HTTP_201_CREATED, ) async def create_key( consumer_id: str, body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None, ) -> dict: await _protected(request, "credentials", authorization, write=True) return await _store(request).create_key(consumer_id, body.get("scopes")) @router.post("/api/v2/token") async def token(request: Request, authorization: AuthorizationHeader = None) -> dict: await _protected(request, "credentials", authorization) return {"access_token": "already-authenticated", "token_type": "bearer"} @router.get("/api/v2/widgets") async def list_widgets( request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "interaction", authorization) return _page(await _store(request).list_widgets()) @router.post("/api/v2/widgets", status_code=status.HTTP_201_CREATED) async def create_widget( body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "interaction", authorization, write=True) return await _store(request).create_widget(body) @router.get("/api/v2/interaction-events") async def list_interactions( request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "interaction", authorization) return _page(await _store(request).list_interactions()) @router.post("/api/v2/interaction-events", status_code=status.HTTP_201_CREATED) async def create_interaction( body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "interaction", authorization, write=True) return await _store(request).create_interaction(body) for index, path in enumerate(( "annotations", "requirement-candidates", "decision-records", "deployment-records", "outcome-signals", )): async def empty_collection( request: Request, authorization: AuthorizationHeader = None ) -> dict: await _protected(request, "deferred", authorization) return {"data": []} stem = path.replace("-", "_") router.add_api_route( f"/api/v2/{path}", empty_collection, methods=["GET"], operation_id=f"compat_list_{stem}_{index}", ) router.add_api_route( f"/api/v2/{path}", empty_collection, methods=["POST"], status_code=201, operation_id=f"compat_create_{stem}_{index}", ) @router.get("/api/v2/openapi.json", include_in_schema=False) async def openapi_json(request: Request) -> dict[str, Any]: _enabled(request, "system") document = request.app.openapi() paths = document.setdefault("paths", {}) for path, operations in list(paths.items()): if path.startswith("/api/v2/"): paths.setdefault(path.removeprefix("/api/v2"), operations) return document @router.get("/api/v2/openapi.yaml", include_in_schema=False) async def openapi_yaml(request: Request) -> PlainTextResponse: document = await openapi_json(request) info = document.get("info", {}) return PlainTextResponse( "openapi: 3.1.0\ninfo:\n" f" title: {info.get('title', 'Hub Core Runtime')}\n" f" version: {info.get('version', '0.2.0')}\n", media_type="application/yaml", ) @router.get("/api/v2/docs", include_in_schema=False) async def docs(request: Request) -> RedirectResponse: _enabled(request, "system") return RedirectResponse(url="/docs") @router.get("/console", response_class=HTMLResponse) async def console( request: Request, authorization: AuthorizationHeader = None ) -> HTMLResponse: await _protected(request, "operator", authorization) return HTMLResponse( "Hub Core Operator Console" "

Hub Core Operator Console

" "

Compatibility runtime candidate; projections are authoritative.

" ) # Core Hub historically served the same compatibility operations both below # /api/v2 and at the root. Keep the aliases as real routes (not merely # OpenAPI aliases) so a route-group cutover cannot turn existing clients into # 404s. The canonical /api/v2 routes remain the only schema-bearing routes; # openapi_json adds the documented alias paths deterministically. for route in list(router.routes): if not isinstance(route, APIRoute) or not route.path.startswith("/api/v2/"): continue alias = route.path.removeprefix("/api/v2") router.add_api_route( alias, route.endpoint, methods=sorted(route.methods or []), status_code=route.status_code, response_class=route.response_class, include_in_schema=False, name=f"legacy_{route.name}", ) return router def _enabled(request: Request, group: str) -> None: if group not in request.app.state.settings.v2_groups: raise HTTPException(status_code=404, detail="compatibility group is disabled") async def _protected( request: Request, group: str, authorization: str | None, *, write: bool = False, ) -> None: _enabled(request, group) if write and group not in request.app.state.settings.v2_write_groups: raise HTTPException(status_code=503, detail="compatibility group is read-only") if not authorization or not authorization.startswith("Bearer "): raise _unauthorized("Missing bearer token") token = authorization.removeprefix("Bearer ").strip() configured = request.app.state.settings.api_token if configured and hmac.compare_digest(token, configured): return try: valid = await _store(request).validate_token(token) except Exception as exc: raise HTTPException(status_code=503, detail="authorization dependency unavailable") from exc if not valid: raise _unauthorized("Invalid bearer token") def _store(request: Request) -> SQLCompatibilityStore: store = request.app.state.compat_store if store is None: raise HTTPException(status_code=503, detail="compatibility store unavailable") return store def _unauthorized(message: str) -> HTTPException: return HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={"code": "unauthorized", "message": message}, ) def _page(values: list[dict[str, Any]]) -> dict[str, Any]: return {"data": values, "count": len(values)} def _hub(row: Any) -> dict[str, Any]: value = dict(row["body"] or {}) value.update( id=row["id"], slug=row["slug"], name=row["name"], domain=row["domain"], hubKind=row["hub_kind"], hubFamily=row["hub_family"], vsmFunction=row["vsm_function"], vsmSystem=row["vsm_system"], status=row["status"], description=row["description"], ) return {key: item for key, item in value.items() if item is not None} def _manifest(row: Any) -> dict[str, Any]: value = dict(row["body"] or {}) value.update( id=row["id"], hubId=row["hub_id"], hubSlug=row["hub_slug"], manifestVersion=row["manifest_version"], status=row["status"], ) return {key: item for key, item in value.items() if item is not None} def _consumer(row: Any) -> dict[str, Any]: value = dict(row["body"] or {}) value.update( id=row["id"], slug=row["slug"], name=row["name"], description=row["description"], hubCapabilityManifestId=row["hub_capability_manifest_id"], rateLimitPerMinute=row["rate_limit_per_minute"], quotaPerDay=row["quota_per_day"], keyPrefix=row["key_prefix"], status=row["status"], ) return {key: item for key, item in value.items() if item is not None} def _widget(row: Any) -> dict[str, Any]: value = dict(row["body"] or {}) value.update( id=row["id"], hubId=row["hub_id"], name=row["name"], widgetType=row["widget_type"], capabilityRef=row["capability_ref"], viewContext=row["view_context"], policyScope=row["policy_scope"], status=row["status"], ) return {key: item for key, item in value.items() if item is not None} def _hash_token(token: str) -> str: return hashlib.sha256(token.encode()).hexdigest() def _now() -> datetime: return datetime.now(timezone.utc)