"""Confluence webhook HTTP routes (Atlassian Connect app). The Confluence trigger is a private Atlassian Connect app, so the descriptor and install/uninstall lifecycle callbacks (``/connect/*``) live here alongside the JWT-verified ``comment_created`` webhook. JWT/qsh verification machinery stays in ``agent.utils.atlassian_connect``. """ from fastapi import APIRouter from . import common from . import confluence as service router = APIRouter() @router.get("/connect/atlassian-connect.json") async def connect_descriptor() -> dict[str, common.Any]: """Serve the Atlassian Connect app descriptor (baseUrl from CONNECT_BASE_URL). signed-install is true: Atlassian asymmetrically (RS256) signs the lifecycle callbacks, so install/uninstall are cryptographically authenticated against Atlassian's published keys (no trust-on-first-use). The comment_created webhook stays symmetric (HS256 against the stored per-tenant sharedSecret). """ return { "key": "sea-haven-open-swe-confluence", "name": "Open SWE", "description": "Triggers Open SWE runs from Confluence comments mentioning @openswe.", "baseUrl": common.CONNECT_BASE_URL, "vendor": {"name": "Sea Haven Industries", "url": "https://seahavenind.com"}, "authentication": {"type": "jwt"}, "apiMigrations": {"signed-install": True, "gdpr": True}, "lifecycle": {"installed": "/connect/installed", "uninstalled": "/connect/uninstalled"}, "scopes": ["READ"], "modules": { "webhooks": [{"event": "comment_created", "url": "/connect/webhook/comment-created"}] }, } @router.post("/connect/installed") async def connect_installed(request: common.Request) -> common.Response: """Connect install lifecycle: trust-on-first-use (host-gated), verify re-install.""" try: body = await request.json() except Exception: # noqa: BLE001 raise common.HTTPException(status_code=400, detail="Invalid JSON") from None code, detail = await service.process_install(request, body) if code >= 400: raise common.HTTPException(status_code=code, detail=detail) return common.Response(status_code=code) @router.post("/connect/uninstalled") async def connect_uninstalled(request: common.Request) -> common.Response: """Connect uninstall lifecycle: verify against the stored secret before deleting.""" try: body = await request.json() except Exception: # noqa: BLE001 raise common.HTTPException(status_code=400, detail="Invalid JSON") from None code, detail = await service.process_uninstall(request, body) if code >= 400: raise common.HTTPException(status_code=code, detail=detail) return common.Response(status_code=code) @router.post("/connect/webhook/comment-created") async def connect_comment_created( request: common.Request, background_tasks: common.BackgroundTasks ) -> dict[str, str]: """JWT-verified Confluence comment_created trigger.""" claims = await common.verify_connect_webhook(request) if claims is None: raise common.HTTPException(status_code=401, detail="Invalid Connect JWT") try: payload = await request.json() except Exception: # noqa: BLE001 return {"status": "error", "message": "Invalid JSON"} background_tasks.add_task(service.process_confluence_comment, payload, claims.get("iss", "")) return {"status": "accepted"}