mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
82 lines
3.4 KiB
Python
82 lines
3.4 KiB
Python
|
|
"""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"}
|