BloxServer API (FastAPI + SQLAlchemy async): - Database models: users, flows, triggers, executions, usage tracking - Clerk JWT auth with dev mode bypass for local testing - SQLite support for local dev, PostgreSQL for production - CRUD routes for flows, triggers, executions - Public webhook endpoint with token auth - Health/readiness endpoints - Pydantic schemas with camelCase aliases for frontend - Docker + docker-compose setup Architecture documentation: - Librarian architecture with RLM-powered query engine - Stripe billing integration (usage-based, trials, webhooks) - LLM abstraction layer (rate limiting, semantic cache, failover) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.6 KiB
Python
125 lines
3.6 KiB
Python
"""
|
|
Webhook trigger endpoint.
|
|
|
|
This handles incoming webhook requests that trigger flows.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, status
|
|
from sqlalchemy import select
|
|
|
|
from bloxserver.api.models.database import get_db_context
|
|
from bloxserver.api.models.tables import (
|
|
ExecutionRecord,
|
|
ExecutionStatus,
|
|
FlowRecord,
|
|
TriggerRecord,
|
|
TriggerType,
|
|
)
|
|
|
|
router = APIRouter(prefix="/webhooks", tags=["webhooks"])
|
|
|
|
|
|
@router.post("/{webhook_token}")
|
|
async def handle_webhook(
|
|
webhook_token: str,
|
|
request: Request,
|
|
) -> dict:
|
|
"""
|
|
Handle incoming webhook request.
|
|
|
|
This endpoint is public (no auth) - the token IS the authentication.
|
|
"""
|
|
async with get_db_context() as db:
|
|
# Look up trigger by token
|
|
query = select(TriggerRecord).where(
|
|
TriggerRecord.webhook_token == webhook_token,
|
|
TriggerRecord.type == TriggerType.WEBHOOK,
|
|
)
|
|
result = await db.execute(query)
|
|
trigger = result.scalar_one_or_none()
|
|
|
|
if not trigger:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found",
|
|
)
|
|
|
|
# Get the flow
|
|
flow_query = select(FlowRecord).where(FlowRecord.id == trigger.flow_id)
|
|
flow = (await db.execute(flow_query)).scalar_one_or_none()
|
|
|
|
if not flow:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Flow not found",
|
|
)
|
|
|
|
if flow.status != "running":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Flow is not running (status: {flow.status})",
|
|
)
|
|
|
|
# Get request body
|
|
try:
|
|
body = await request.body()
|
|
input_payload = body.decode("utf-8") if body else None
|
|
except Exception:
|
|
input_payload = None
|
|
|
|
# Create execution record
|
|
execution = ExecutionRecord(
|
|
flow_id=flow.id,
|
|
trigger_id=trigger.id,
|
|
trigger_type=TriggerType.WEBHOOK,
|
|
status=ExecutionStatus.RUNNING,
|
|
input_payload=input_payload,
|
|
)
|
|
db.add(execution)
|
|
await db.commit()
|
|
|
|
# TODO: Actually dispatch to the running container
|
|
# This would send the payload to the flow's container
|
|
|
|
return {
|
|
"status": "accepted",
|
|
"executionId": str(execution.id),
|
|
"message": "Webhook received and execution started",
|
|
}
|
|
|
|
|
|
@router.get("/{webhook_token}/test")
|
|
async def test_webhook(webhook_token: str) -> dict:
|
|
"""
|
|
Test that a webhook token is valid.
|
|
|
|
Returns info about the trigger without actually executing.
|
|
"""
|
|
async with get_db_context() as db:
|
|
query = select(TriggerRecord).where(
|
|
TriggerRecord.webhook_token == webhook_token,
|
|
TriggerRecord.type == TriggerType.WEBHOOK,
|
|
)
|
|
result = await db.execute(query)
|
|
trigger = result.scalar_one_or_none()
|
|
|
|
if not trigger:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Webhook not found",
|
|
)
|
|
|
|
# Get the flow
|
|
flow_query = select(FlowRecord).where(FlowRecord.id == trigger.flow_id)
|
|
flow = (await db.execute(flow_query)).scalar_one_or_none()
|
|
|
|
return {
|
|
"valid": True,
|
|
"triggerName": trigger.name,
|
|
"flowName": flow.name if flow else None,
|
|
"flowStatus": flow.status.value if flow else None,
|
|
}
|