langchain-trigger-server 0.2.6rc8__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langchain-trigger-server might be problematic. Click here for more details.
- {langchain_trigger_server-0.2.6rc8.dist-info → langchain_trigger_server-0.2.7.dist-info}/METADATA +4 -5
- langchain_trigger_server-0.2.7.dist-info/RECORD +15 -0
- langchain_triggers/__init__.py +8 -3
- langchain_triggers/app.py +345 -253
- langchain_triggers/auth/__init__.py +3 -4
- langchain_triggers/auth/slack_hmac.py +21 -26
- langchain_triggers/core.py +51 -27
- langchain_triggers/cron_manager.py +79 -56
- langchain_triggers/database/__init__.py +2 -2
- langchain_triggers/database/interface.py +55 -68
- langchain_triggers/database/supabase.py +217 -159
- langchain_triggers/decorators.py +52 -25
- langchain_triggers/triggers/__init__.py +1 -1
- langchain_triggers/triggers/cron_trigger.py +11 -11
- langchain_trigger_server-0.2.6rc8.dist-info/RECORD +0 -15
- {langchain_trigger_server-0.2.6rc8.dist-info → langchain_trigger_server-0.2.7.dist-info}/WHEEL +0 -0
langchain_triggers/app.py
CHANGED
|
@@ -4,68 +4,71 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
-
from
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
|
-
from fastapi import FastAPI, HTTPException, Request
|
|
10
|
-
from langgraph_sdk import get_client
|
|
10
|
+
from fastapi import Depends, FastAPI, HTTPException, Request
|
|
11
11
|
from langchain_auth.client import Client
|
|
12
|
+
from langgraph_sdk import get_client
|
|
12
13
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
14
|
from starlette.responses import Response
|
|
14
15
|
|
|
15
|
-
from .decorators import TriggerTemplate
|
|
16
|
-
from .database import create_database, TriggerDatabaseInterface
|
|
17
|
-
from .cron_manager import CronTriggerManager
|
|
18
|
-
from .triggers.cron_trigger import CRON_TRIGGER_ID
|
|
19
16
|
from .auth.slack_hmac import (
|
|
20
|
-
verify_slack_signature,
|
|
21
|
-
get_slack_signing_secret,
|
|
22
|
-
extract_slack_headers,
|
|
23
17
|
SlackSignatureVerificationError,
|
|
18
|
+
extract_slack_headers,
|
|
19
|
+
get_slack_signing_secret,
|
|
20
|
+
verify_slack_signature,
|
|
24
21
|
)
|
|
22
|
+
from .cron_manager import CronTriggerManager
|
|
23
|
+
from .database import TriggerDatabaseInterface, create_database
|
|
24
|
+
from .decorators import TriggerTemplate
|
|
25
|
+
from .triggers.cron_trigger import CRON_TRIGGER_ID
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
30
31
|
"""Middleware to handle authentication for API endpoints."""
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
def __init__(self, app, auth_handler: Callable):
|
|
33
34
|
super().__init__(app)
|
|
34
35
|
self.auth_handler = auth_handler
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
async def dispatch(self, request: Request, call_next):
|
|
37
38
|
# Skip auth for webhooks, health/root endpoints, and OPTIONS requests
|
|
38
|
-
if (
|
|
39
|
-
request.url.path
|
|
40
|
-
request.
|
|
39
|
+
if (
|
|
40
|
+
request.url.path.startswith("/v1/triggers/webhooks/")
|
|
41
|
+
or request.url.path in ["/", "/health"]
|
|
42
|
+
or request.method == "OPTIONS"
|
|
43
|
+
):
|
|
41
44
|
return await call_next(request)
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
try:
|
|
44
47
|
# Run mandatory custom authentication
|
|
45
48
|
identity = await self.auth_handler({}, dict(request.headers))
|
|
46
|
-
|
|
49
|
+
|
|
47
50
|
if not identity or not identity.get("identity"):
|
|
48
51
|
return Response(
|
|
49
52
|
content='{"detail": "Authentication required"}',
|
|
50
53
|
status_code=401,
|
|
51
|
-
media_type="application/json"
|
|
54
|
+
media_type="application/json",
|
|
52
55
|
)
|
|
53
|
-
|
|
56
|
+
|
|
54
57
|
# Store identity in request state for endpoints to access
|
|
55
58
|
request.state.current_user = identity
|
|
56
|
-
|
|
59
|
+
|
|
57
60
|
except Exception as e:
|
|
58
61
|
logger.error(f"Authentication middleware error: {e}")
|
|
59
62
|
return Response(
|
|
60
63
|
content='{"detail": "Authentication failed"}',
|
|
61
64
|
status_code=401,
|
|
62
|
-
media_type="application/json"
|
|
65
|
+
media_type="application/json",
|
|
63
66
|
)
|
|
64
|
-
|
|
67
|
+
|
|
65
68
|
return await call_next(request)
|
|
66
69
|
|
|
67
70
|
|
|
68
|
-
def get_current_user(request: Request) ->
|
|
71
|
+
def get_current_user(request: Request) -> dict[str, Any]:
|
|
69
72
|
"""FastAPI dependency to get the current authenticated user."""
|
|
70
73
|
if not hasattr(request.state, "current_user"):
|
|
71
74
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
@@ -74,12 +77,12 @@ def get_current_user(request: Request) -> Dict[str, Any]:
|
|
|
74
77
|
|
|
75
78
|
class TriggerServer:
|
|
76
79
|
"""FastAPI application for trigger webhooks."""
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
def __init__(
|
|
79
82
|
self,
|
|
80
83
|
auth_handler: Callable,
|
|
81
|
-
database:
|
|
82
|
-
database_type:
|
|
84
|
+
database: TriggerDatabaseInterface | None = None,
|
|
85
|
+
database_type: str | None = "supabase",
|
|
83
86
|
**database_kwargs: Any,
|
|
84
87
|
):
|
|
85
88
|
# Configure uvicorn logging to use consistent formatting
|
|
@@ -88,7 +91,7 @@ class TriggerServer:
|
|
|
88
91
|
self.app = FastAPI(
|
|
89
92
|
title="Triggers Server",
|
|
90
93
|
description="Event-driven triggers framework",
|
|
91
|
-
version="0.1.0"
|
|
94
|
+
version="0.1.0",
|
|
92
95
|
)
|
|
93
96
|
|
|
94
97
|
# Configure database: allow either instance injection or factory creation
|
|
@@ -100,104 +103,117 @@ class TriggerServer:
|
|
|
100
103
|
else:
|
|
101
104
|
self.database = create_database(database_type, **database_kwargs)
|
|
102
105
|
self.auth_handler = auth_handler
|
|
103
|
-
|
|
106
|
+
|
|
104
107
|
# LangGraph configuration
|
|
105
108
|
self.langgraph_api_url = os.getenv("LANGGRAPH_API_URL")
|
|
106
109
|
self.langsmith_api_key = os.getenv("LANGCHAIN_API_KEY")
|
|
107
110
|
self.trigger_server_auth_api_url = os.getenv("TRIGGER_SERVER_HOST_API_URL")
|
|
108
|
-
|
|
111
|
+
|
|
109
112
|
if not self.langgraph_api_url:
|
|
110
113
|
raise ValueError("LANGGRAPH_API_URL environment variable is required")
|
|
111
|
-
|
|
114
|
+
|
|
112
115
|
self.langgraph_api_url = self.langgraph_api_url.rstrip("/")
|
|
113
116
|
|
|
114
117
|
# Initialize LangGraph SDK client
|
|
115
|
-
self.langgraph_client = get_client(
|
|
116
|
-
|
|
118
|
+
self.langgraph_client = get_client(
|
|
119
|
+
url=self.langgraph_api_url, api_key=self.langsmith_api_key
|
|
120
|
+
)
|
|
121
|
+
logger.info(
|
|
122
|
+
f"✓ LangGraph client initialized with URL: {self.langgraph_api_url}"
|
|
123
|
+
)
|
|
117
124
|
if self.langsmith_api_key:
|
|
118
|
-
logger.info(
|
|
125
|
+
logger.info("✓ LangGraph client initialized with API key.")
|
|
119
126
|
else:
|
|
120
127
|
logger.warning("⚠ LangGraph client initialized without API key")
|
|
121
128
|
|
|
122
129
|
# Initialize LangChain auth client
|
|
123
130
|
langchain_api_key = os.getenv("LANGCHAIN_API_KEY")
|
|
124
131
|
if langchain_api_key:
|
|
125
|
-
self.langchain_auth_client = Client(
|
|
132
|
+
self.langchain_auth_client = Client(
|
|
133
|
+
api_key=langchain_api_key, api_url=self.trigger_server_auth_api_url
|
|
134
|
+
)
|
|
126
135
|
logger.info("✓ LangChain auth client initialized")
|
|
127
136
|
else:
|
|
128
137
|
self.langchain_auth_client = None
|
|
129
|
-
logger.warning(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
logger.warning(
|
|
139
|
+
"LANGCHAIN_API_KEY not found - OAuth token injection disabled"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self.triggers: list[TriggerTemplate] = []
|
|
143
|
+
|
|
133
144
|
# Initialize CronTriggerManager
|
|
134
145
|
self.cron_manager = CronTriggerManager(self)
|
|
135
|
-
|
|
146
|
+
|
|
136
147
|
# Setup authentication middleware
|
|
137
148
|
self.app.add_middleware(AuthenticationMiddleware, auth_handler=auth_handler)
|
|
138
|
-
|
|
149
|
+
|
|
139
150
|
# Setup routes
|
|
140
151
|
self._setup_routes()
|
|
141
|
-
|
|
152
|
+
|
|
142
153
|
# Add startup and shutdown events
|
|
143
154
|
@self.app.on_event("startup")
|
|
144
155
|
async def startup_event():
|
|
145
156
|
await self.ensure_trigger_templates()
|
|
146
157
|
await self.cron_manager.start()
|
|
147
|
-
|
|
158
|
+
|
|
148
159
|
@self.app.on_event("shutdown")
|
|
149
160
|
async def shutdown_event():
|
|
150
161
|
await self.cron_manager.shutdown()
|
|
151
|
-
|
|
162
|
+
|
|
152
163
|
def _configure_uvicorn_logging(self) -> None:
|
|
153
164
|
"""Configure uvicorn loggers to use consistent formatting for production deployments."""
|
|
154
165
|
formatter = logging.Formatter("%(levelname)s: %(name)s - %(message)s")
|
|
155
|
-
|
|
166
|
+
|
|
156
167
|
# Configure uvicorn access logger
|
|
157
168
|
uvicorn_access_logger = logging.getLogger("uvicorn.access")
|
|
158
169
|
uvicorn_access_logger.handlers.clear()
|
|
159
170
|
access_handler = logging.StreamHandler()
|
|
160
171
|
access_handler.setFormatter(formatter)
|
|
161
172
|
uvicorn_access_logger.addHandler(access_handler)
|
|
162
|
-
|
|
163
|
-
# Configure uvicorn error logger
|
|
173
|
+
|
|
174
|
+
# Configure uvicorn error logger
|
|
164
175
|
uvicorn_error_logger = logging.getLogger("uvicorn.error")
|
|
165
176
|
uvicorn_error_logger.handlers.clear()
|
|
166
177
|
error_handler = logging.StreamHandler()
|
|
167
178
|
error_handler.setFormatter(formatter)
|
|
168
179
|
uvicorn_error_logger.addHandler(error_handler)
|
|
169
|
-
|
|
180
|
+
|
|
170
181
|
# Configure uvicorn main logger
|
|
171
182
|
uvicorn_logger = logging.getLogger("uvicorn")
|
|
172
183
|
uvicorn_logger.handlers.clear()
|
|
173
184
|
main_handler = logging.StreamHandler()
|
|
174
185
|
main_handler.setFormatter(formatter)
|
|
175
186
|
uvicorn_logger.addHandler(main_handler)
|
|
176
|
-
|
|
187
|
+
|
|
177
188
|
def add_trigger(self, trigger: TriggerTemplate) -> None:
|
|
178
189
|
"""Add a trigger template to the app."""
|
|
179
190
|
# Check for duplicate IDs
|
|
180
191
|
if any(t.id == trigger.id for t in self.triggers):
|
|
181
192
|
raise ValueError(f"Trigger with id '{trigger.id}' already exists")
|
|
182
|
-
|
|
193
|
+
|
|
183
194
|
self.triggers.append(trigger)
|
|
184
195
|
|
|
185
196
|
if trigger.trigger_handler:
|
|
186
|
-
|
|
197
|
+
|
|
198
|
+
async def handler_endpoint(request: Request) -> dict[str, Any]:
|
|
187
199
|
return await self._handle_request(trigger, request)
|
|
188
|
-
|
|
200
|
+
|
|
189
201
|
handler_path = f"/v1/triggers/webhooks/{trigger.id}"
|
|
190
202
|
self.app.post(handler_path)(handler_endpoint)
|
|
191
203
|
logger.info(f"Added handler route: POST {handler_path}")
|
|
192
|
-
|
|
193
|
-
logger.info(
|
|
194
|
-
|
|
204
|
+
|
|
205
|
+
logger.info(
|
|
206
|
+
f"Registered trigger template in memory: {trigger.name} ({trigger.id})"
|
|
207
|
+
)
|
|
208
|
+
|
|
195
209
|
async def ensure_trigger_templates(self) -> None:
|
|
196
210
|
"""Ensure all registered trigger templates exist in the database."""
|
|
197
211
|
for trigger in self.triggers:
|
|
198
212
|
existing = await self.database.get_trigger_template(trigger.id)
|
|
199
213
|
if not existing:
|
|
200
|
-
logger.info(
|
|
214
|
+
logger.info(
|
|
215
|
+
f"Creating new trigger template in database: {trigger.name} ({trigger.id})"
|
|
216
|
+
)
|
|
201
217
|
await self.database.create_trigger_template(
|
|
202
218
|
id=trigger.id,
|
|
203
219
|
provider=trigger.provider,
|
|
@@ -205,81 +221,91 @@ class TriggerServer:
|
|
|
205
221
|
description=trigger.description,
|
|
206
222
|
registration_schema=trigger.registration_model.model_json_schema(),
|
|
207
223
|
)
|
|
208
|
-
logger.info(
|
|
224
|
+
logger.info(
|
|
225
|
+
f"✓ Successfully created trigger template: {trigger.name} ({trigger.id})"
|
|
226
|
+
)
|
|
209
227
|
else:
|
|
210
|
-
logger.info(
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
logger.info(
|
|
229
|
+
f"✓ Trigger template already exists in database: {trigger.name} ({trigger.id})"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def add_triggers(self, triggers: list[TriggerTemplate]) -> None:
|
|
213
233
|
"""Add multiple triggers."""
|
|
214
234
|
for trigger in triggers:
|
|
215
235
|
self.add_trigger(trigger)
|
|
216
|
-
|
|
236
|
+
|
|
217
237
|
def _setup_routes(self) -> None:
|
|
218
238
|
"""Setup built-in API routes."""
|
|
219
|
-
|
|
239
|
+
|
|
220
240
|
@self.app.get("/")
|
|
221
|
-
async def root() ->
|
|
241
|
+
async def root() -> dict[str, str]:
|
|
222
242
|
return {"message": "Triggers Server", "version": "0.1.0"}
|
|
223
|
-
|
|
243
|
+
|
|
224
244
|
@self.app.get("/health")
|
|
225
|
-
async def health() ->
|
|
245
|
+
async def health() -> dict[str, str]:
|
|
226
246
|
return {"status": "healthy"}
|
|
227
|
-
|
|
247
|
+
|
|
228
248
|
@self.app.get("/v1/triggers")
|
|
229
|
-
async def api_list_triggers() ->
|
|
249
|
+
async def api_list_triggers() -> dict[str, Any]:
|
|
230
250
|
"""List available trigger templates."""
|
|
231
251
|
templates = await self.database.get_trigger_templates()
|
|
232
252
|
trigger_list = []
|
|
233
253
|
for template in templates:
|
|
234
|
-
trigger_list.append(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
trigger_list.append(
|
|
255
|
+
{
|
|
256
|
+
"id": template["id"],
|
|
257
|
+
"provider": template["provider"],
|
|
258
|
+
"displayName": template["name"],
|
|
259
|
+
"description": template["description"],
|
|
260
|
+
"path": "/v1/triggers/registrations",
|
|
261
|
+
"method": "POST",
|
|
262
|
+
"payloadSchema": template.get("registration_schema", {}),
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return {"success": True, "data": trigger_list}
|
|
267
|
+
|
|
249
268
|
@self.app.get("/v1/triggers/registrations")
|
|
250
|
-
async def api_list_registrations(
|
|
269
|
+
async def api_list_registrations(
|
|
270
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
271
|
+
) -> dict[str, Any]:
|
|
251
272
|
"""List user's trigger registrations (user-scoped)."""
|
|
252
273
|
try:
|
|
253
274
|
user_id = current_user["identity"]
|
|
254
|
-
|
|
275
|
+
|
|
255
276
|
# Get user's trigger registrations with linked agents in a single query
|
|
256
|
-
user_registrations =
|
|
257
|
-
|
|
277
|
+
user_registrations = (
|
|
278
|
+
await self.database.get_user_trigger_registrations_with_agents(
|
|
279
|
+
user_id
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
258
283
|
# Format response to match expected structure
|
|
259
284
|
registrations = []
|
|
260
285
|
for reg in user_registrations:
|
|
261
|
-
registrations.append(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
registrations.append(
|
|
287
|
+
{
|
|
288
|
+
"id": reg["id"],
|
|
289
|
+
"user_id": reg["user_id"],
|
|
290
|
+
"template_id": reg.get("trigger_templates", {}).get("id"),
|
|
291
|
+
"resource": reg["resource"],
|
|
292
|
+
"linked_agent_ids": reg.get("linked_agent_ids", []),
|
|
293
|
+
"created_at": reg["created_at"],
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return {"success": True, "data": registrations}
|
|
298
|
+
|
|
275
299
|
except HTTPException:
|
|
276
300
|
raise
|
|
277
301
|
except Exception as e:
|
|
278
302
|
logger.error(f"Error listing registrations: {e}")
|
|
279
303
|
raise HTTPException(status_code=500, detail=str(e))
|
|
280
|
-
|
|
304
|
+
|
|
281
305
|
@self.app.post("/v1/triggers/registrations")
|
|
282
|
-
async def api_create_registration(
|
|
306
|
+
async def api_create_registration(
|
|
307
|
+
request: Request, current_user: dict[str, Any] = Depends(get_current_user)
|
|
308
|
+
) -> dict[str, Any]:
|
|
283
309
|
"""Create a new trigger registration."""
|
|
284
310
|
try:
|
|
285
311
|
payload = await request.json()
|
|
@@ -288,100 +314,126 @@ class TriggerServer:
|
|
|
288
314
|
user_id = current_user["identity"]
|
|
289
315
|
trigger_id = payload.get("type")
|
|
290
316
|
if not trigger_id:
|
|
291
|
-
raise HTTPException(
|
|
292
|
-
|
|
317
|
+
raise HTTPException(
|
|
318
|
+
status_code=400, detail="Missing required field: type"
|
|
319
|
+
)
|
|
320
|
+
|
|
293
321
|
trigger = next((t for t in self.triggers if t.id == trigger_id), None)
|
|
294
322
|
if not trigger:
|
|
295
|
-
raise HTTPException(
|
|
323
|
+
raise HTTPException(
|
|
324
|
+
status_code=400, detail=f"Unknown trigger type: {trigger_id}"
|
|
325
|
+
)
|
|
296
326
|
|
|
297
327
|
# Parse payload into registration model first
|
|
298
328
|
try:
|
|
299
329
|
registration_instance = trigger.registration_model(**payload)
|
|
300
330
|
except Exception as e:
|
|
301
331
|
raise HTTPException(
|
|
302
|
-
status_code=400,
|
|
303
|
-
detail=f"Invalid payload for trigger: {str(e)}"
|
|
332
|
+
status_code=400, detail=f"Invalid payload for trigger: {str(e)}"
|
|
304
333
|
)
|
|
305
|
-
|
|
334
|
+
|
|
306
335
|
# Check for duplicate registration based on resource data within this user's scope
|
|
307
336
|
resource_dict = registration_instance.model_dump()
|
|
308
|
-
existing_registration =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
337
|
+
existing_registration = (
|
|
338
|
+
await self.database.find_user_registration_by_resource(
|
|
339
|
+
user_id=user_id,
|
|
340
|
+
template_id=trigger.id,
|
|
341
|
+
resource_data=resource_dict,
|
|
342
|
+
)
|
|
312
343
|
)
|
|
313
344
|
|
|
314
345
|
if existing_registration:
|
|
315
346
|
raise HTTPException(
|
|
316
347
|
status_code=400,
|
|
317
|
-
detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}"
|
|
348
|
+
detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}",
|
|
318
349
|
)
|
|
319
|
-
result = await trigger.registration_handler(
|
|
320
|
-
|
|
350
|
+
result = await trigger.registration_handler(
|
|
351
|
+
user_id, self.langchain_auth_client, registration_instance
|
|
352
|
+
)
|
|
353
|
+
|
|
321
354
|
# Check if handler requested to skip registration (e.g., for OAuth or URL verification)
|
|
322
355
|
if not result.create_registration:
|
|
323
|
-
logger.info(
|
|
324
|
-
|
|
356
|
+
logger.info(
|
|
357
|
+
"Registration handler requested to skip database creation"
|
|
358
|
+
)
|
|
325
359
|
import json
|
|
360
|
+
|
|
361
|
+
from fastapi import Response
|
|
362
|
+
|
|
326
363
|
return Response(
|
|
327
364
|
content=json.dumps(result.response_body),
|
|
328
365
|
status_code=result.status_code,
|
|
329
|
-
media_type="application/json"
|
|
366
|
+
media_type="application/json",
|
|
330
367
|
)
|
|
331
|
-
|
|
368
|
+
|
|
332
369
|
resource_dict = registration_instance.model_dump()
|
|
333
370
|
|
|
334
371
|
registration = await self.database.create_trigger_registration(
|
|
335
372
|
user_id=user_id,
|
|
336
373
|
template_id=trigger.id,
|
|
337
374
|
resource=resource_dict,
|
|
338
|
-
metadata=result.metadata
|
|
375
|
+
metadata=result.metadata,
|
|
339
376
|
)
|
|
340
|
-
|
|
377
|
+
|
|
341
378
|
if not registration:
|
|
342
|
-
raise HTTPException(
|
|
343
|
-
|
|
379
|
+
raise HTTPException(
|
|
380
|
+
status_code=500, detail="Failed to create trigger registration"
|
|
381
|
+
)
|
|
382
|
+
|
|
344
383
|
# Reload cron manager to pick up any new cron registrations
|
|
345
384
|
await self.cron_manager.reload_from_database()
|
|
346
|
-
|
|
385
|
+
|
|
347
386
|
# Return registration result
|
|
348
387
|
return {
|
|
349
388
|
"success": True,
|
|
350
389
|
"data": registration,
|
|
351
|
-
"metadata": result.metadata
|
|
390
|
+
"metadata": result.metadata,
|
|
352
391
|
}
|
|
353
|
-
|
|
392
|
+
|
|
354
393
|
except HTTPException:
|
|
355
394
|
raise
|
|
356
395
|
except Exception as e:
|
|
357
396
|
logger.exception(f"Error creating trigger registration: {e}")
|
|
358
397
|
raise HTTPException(status_code=500, detail=str(e))
|
|
359
|
-
|
|
398
|
+
|
|
360
399
|
@self.app.get("/v1/triggers/registrations/{registration_id}/agents")
|
|
361
|
-
async def api_list_registration_agents(
|
|
400
|
+
async def api_list_registration_agents(
|
|
401
|
+
registration_id: str,
|
|
402
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
403
|
+
) -> dict[str, Any]:
|
|
362
404
|
"""List agents linked to this registration."""
|
|
363
405
|
try:
|
|
364
406
|
user_id = current_user["identity"]
|
|
365
|
-
|
|
407
|
+
|
|
366
408
|
# Get the specific trigger registration
|
|
367
|
-
trigger = await self.database.get_trigger_registration(
|
|
409
|
+
trigger = await self.database.get_trigger_registration(
|
|
410
|
+
registration_id, user_id
|
|
411
|
+
)
|
|
368
412
|
if not trigger:
|
|
369
|
-
raise HTTPException(
|
|
370
|
-
|
|
413
|
+
raise HTTPException(
|
|
414
|
+
status_code=404,
|
|
415
|
+
detail="Trigger registration not found or access denied",
|
|
416
|
+
)
|
|
417
|
+
|
|
371
418
|
# Return the linked agent IDs
|
|
372
419
|
return {
|
|
373
420
|
"success": True,
|
|
374
|
-
"data": trigger.get("linked_assistant_ids", [])
|
|
421
|
+
"data": trigger.get("linked_assistant_ids", []),
|
|
375
422
|
}
|
|
376
|
-
|
|
423
|
+
|
|
377
424
|
except HTTPException:
|
|
378
425
|
raise
|
|
379
426
|
except Exception as e:
|
|
380
427
|
logger.error(f"Error getting registration agents: {e}")
|
|
381
428
|
raise HTTPException(status_code=500, detail=str(e))
|
|
382
|
-
|
|
429
|
+
|
|
383
430
|
@self.app.post("/v1/triggers/registrations/{registration_id}/agents/{agent_id}")
|
|
384
|
-
async def api_add_agent_to_trigger(
|
|
431
|
+
async def api_add_agent_to_trigger(
|
|
432
|
+
registration_id: str,
|
|
433
|
+
agent_id: str,
|
|
434
|
+
request: Request,
|
|
435
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
436
|
+
) -> dict[str, Any]:
|
|
385
437
|
"""Add an agent to a trigger registration."""
|
|
386
438
|
try:
|
|
387
439
|
# Parse request body for field selection
|
|
@@ -390,67 +442,80 @@ class TriggerServer:
|
|
|
390
442
|
field_selection = body.get("field_selection")
|
|
391
443
|
except:
|
|
392
444
|
field_selection = None
|
|
393
|
-
|
|
445
|
+
|
|
394
446
|
user_id = current_user["identity"]
|
|
395
|
-
|
|
447
|
+
|
|
396
448
|
# Verify the trigger registration exists and belongs to the user
|
|
397
|
-
registration = await self.database.get_trigger_registration(
|
|
449
|
+
registration = await self.database.get_trigger_registration(
|
|
450
|
+
registration_id, user_id
|
|
451
|
+
)
|
|
398
452
|
if not registration:
|
|
399
|
-
raise HTTPException(
|
|
400
|
-
|
|
453
|
+
raise HTTPException(
|
|
454
|
+
status_code=404,
|
|
455
|
+
detail="Trigger registration not found or access denied",
|
|
456
|
+
)
|
|
457
|
+
|
|
401
458
|
# Link the agent to the trigger
|
|
402
459
|
success = await self.database.link_agent_to_trigger(
|
|
403
460
|
agent_id=agent_id,
|
|
404
461
|
registration_id=registration_id,
|
|
405
462
|
created_by=user_id,
|
|
406
|
-
field_selection=field_selection
|
|
463
|
+
field_selection=field_selection,
|
|
407
464
|
)
|
|
408
|
-
|
|
465
|
+
|
|
409
466
|
if not success:
|
|
410
|
-
raise HTTPException(
|
|
411
|
-
|
|
467
|
+
raise HTTPException(
|
|
468
|
+
status_code=500, detail="Failed to link agent to trigger"
|
|
469
|
+
)
|
|
470
|
+
|
|
412
471
|
return {
|
|
413
472
|
"success": True,
|
|
414
473
|
"message": f"Successfully linked agent {agent_id} to trigger {registration_id}",
|
|
415
|
-
"data": {
|
|
416
|
-
"registration_id": registration_id,
|
|
417
|
-
"agent_id": agent_id
|
|
418
|
-
}
|
|
474
|
+
"data": {"registration_id": registration_id, "agent_id": agent_id},
|
|
419
475
|
}
|
|
420
|
-
|
|
476
|
+
|
|
421
477
|
except HTTPException:
|
|
422
478
|
raise
|
|
423
479
|
except Exception as e:
|
|
424
480
|
logger.error(f"Error linking agent to trigger: {e}")
|
|
425
481
|
raise HTTPException(status_code=500, detail=str(e))
|
|
426
|
-
|
|
427
|
-
@self.app.delete(
|
|
428
|
-
|
|
482
|
+
|
|
483
|
+
@self.app.delete(
|
|
484
|
+
"/v1/triggers/registrations/{registration_id}/agents/{agent_id}"
|
|
485
|
+
)
|
|
486
|
+
async def api_remove_agent_from_trigger(
|
|
487
|
+
registration_id: str,
|
|
488
|
+
agent_id: str,
|
|
489
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
490
|
+
) -> dict[str, Any]:
|
|
429
491
|
"""Remove an agent from a trigger registration."""
|
|
430
492
|
try:
|
|
431
493
|
user_id = current_user["identity"]
|
|
432
494
|
|
|
433
495
|
# Verify the trigger registration exists and belongs to the user
|
|
434
|
-
registration = await self.database.get_trigger_registration(
|
|
496
|
+
registration = await self.database.get_trigger_registration(
|
|
497
|
+
registration_id, user_id
|
|
498
|
+
)
|
|
435
499
|
if not registration:
|
|
436
|
-
raise HTTPException(
|
|
500
|
+
raise HTTPException(
|
|
501
|
+
status_code=404,
|
|
502
|
+
detail="Trigger registration not found or access denied",
|
|
503
|
+
)
|
|
437
504
|
|
|
438
505
|
# Unlink the agent from the trigger
|
|
439
506
|
success = await self.database.unlink_agent_from_trigger(
|
|
440
|
-
agent_id=agent_id,
|
|
441
|
-
registration_id=registration_id
|
|
507
|
+
agent_id=agent_id, registration_id=registration_id
|
|
442
508
|
)
|
|
443
509
|
|
|
444
510
|
if not success:
|
|
445
|
-
raise HTTPException(
|
|
511
|
+
raise HTTPException(
|
|
512
|
+
status_code=500, detail="Failed to unlink agent from trigger"
|
|
513
|
+
)
|
|
446
514
|
|
|
447
515
|
return {
|
|
448
516
|
"success": True,
|
|
449
517
|
"message": f"Successfully unlinked agent {agent_id} from trigger {registration_id}",
|
|
450
|
-
"data": {
|
|
451
|
-
"registration_id": registration_id,
|
|
452
|
-
"agent_id": agent_id
|
|
453
|
-
}
|
|
518
|
+
"data": {"registration_id": registration_id, "agent_id": agent_id},
|
|
454
519
|
}
|
|
455
520
|
|
|
456
521
|
except HTTPException:
|
|
@@ -460,20 +525,31 @@ class TriggerServer:
|
|
|
460
525
|
raise HTTPException(status_code=500, detail=str(e))
|
|
461
526
|
|
|
462
527
|
@self.app.post("/v1/triggers/registrations/{registration_id}/execute")
|
|
463
|
-
async def api_execute_trigger_now(
|
|
528
|
+
async def api_execute_trigger_now(
|
|
529
|
+
registration_id: str,
|
|
530
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
531
|
+
) -> dict[str, Any]:
|
|
464
532
|
"""Manually execute a cron trigger registration immediately."""
|
|
465
533
|
try:
|
|
466
534
|
user_id = current_user["identity"]
|
|
467
535
|
|
|
468
536
|
# Verify the trigger registration exists and belongs to the user
|
|
469
|
-
registration = await self.database.get_trigger_registration(
|
|
537
|
+
registration = await self.database.get_trigger_registration(
|
|
538
|
+
registration_id, user_id
|
|
539
|
+
)
|
|
470
540
|
if not registration:
|
|
471
|
-
raise HTTPException(
|
|
541
|
+
raise HTTPException(
|
|
542
|
+
status_code=404,
|
|
543
|
+
detail="Trigger registration not found or access denied",
|
|
544
|
+
)
|
|
472
545
|
|
|
473
546
|
# Get the template to check if it's a cron trigger
|
|
474
547
|
template_id = registration.get("template_id")
|
|
475
548
|
if template_id != CRON_TRIGGER_ID:
|
|
476
|
-
raise HTTPException(
|
|
549
|
+
raise HTTPException(
|
|
550
|
+
status_code=400,
|
|
551
|
+
detail="Manual execution is only supported for cron triggers",
|
|
552
|
+
)
|
|
477
553
|
|
|
478
554
|
# Execute the cron trigger using the cron manager
|
|
479
555
|
agents_invoked = await self.cron_manager.execute_cron_job(registration)
|
|
@@ -481,7 +557,7 @@ class TriggerServer:
|
|
|
481
557
|
return {
|
|
482
558
|
"success": True,
|
|
483
559
|
"message": f"Manually executed cron trigger {registration_id}",
|
|
484
|
-
"agents_invoked": agents_invoked
|
|
560
|
+
"agents_invoked": agents_invoked,
|
|
485
561
|
}
|
|
486
562
|
|
|
487
563
|
except HTTPException:
|
|
@@ -489,29 +565,34 @@ class TriggerServer:
|
|
|
489
565
|
except Exception as e:
|
|
490
566
|
logger.error(f"Error executing trigger: {e}")
|
|
491
567
|
raise HTTPException(status_code=500, detail=str(e))
|
|
492
|
-
|
|
493
|
-
|
|
568
|
+
|
|
494
569
|
async def _handle_request(
|
|
495
|
-
self,
|
|
496
|
-
|
|
497
|
-
request: Request
|
|
498
|
-
) -> Dict[str, Any]:
|
|
570
|
+
self, trigger: TriggerTemplate, request: Request
|
|
571
|
+
) -> dict[str, Any]:
|
|
499
572
|
"""Handle an incoming request with a handler function."""
|
|
500
573
|
try:
|
|
501
574
|
if request.method == "POST":
|
|
502
|
-
if request.headers.get("content-type", "").startswith(
|
|
575
|
+
if request.headers.get("content-type", "").startswith(
|
|
576
|
+
"application/json"
|
|
577
|
+
):
|
|
503
578
|
# Read body once for both auth and parsing
|
|
504
579
|
body_bytes = await request.body()
|
|
505
580
|
body_str = body_bytes.decode("utf-8")
|
|
506
|
-
|
|
581
|
+
|
|
507
582
|
if self._is_slack_trigger(trigger):
|
|
508
|
-
await self._verify_slack_webhook_auth_with_body(
|
|
509
|
-
|
|
583
|
+
await self._verify_slack_webhook_auth_with_body(
|
|
584
|
+
request, body_str
|
|
585
|
+
)
|
|
586
|
+
|
|
510
587
|
import json
|
|
588
|
+
|
|
511
589
|
payload = json.loads(body_str)
|
|
512
590
|
|
|
513
|
-
if
|
|
514
|
-
|
|
591
|
+
if (
|
|
592
|
+
payload.get("type") == "url_verification"
|
|
593
|
+
and "challenge" in payload
|
|
594
|
+
):
|
|
595
|
+
logger.info("Responding to Slack URL verification challenge")
|
|
515
596
|
return {"challenge": payload["challenge"]}
|
|
516
597
|
else:
|
|
517
598
|
# Handle form data or other content types
|
|
@@ -521,7 +602,9 @@ class TriggerServer:
|
|
|
521
602
|
payload = dict(request.query_params)
|
|
522
603
|
|
|
523
604
|
query_params = dict(request.query_params)
|
|
524
|
-
result = await trigger.trigger_handler(
|
|
605
|
+
result = await trigger.trigger_handler(
|
|
606
|
+
payload, query_params, self.database, self.langchain_auth_client
|
|
607
|
+
)
|
|
525
608
|
if not result.invoke_agent:
|
|
526
609
|
return result.response_body
|
|
527
610
|
|
|
@@ -532,16 +615,16 @@ class TriggerServer:
|
|
|
532
615
|
# Iterate through each message and invoke agents for each
|
|
533
616
|
for message in result.agent_messages:
|
|
534
617
|
for agent_link in agent_links:
|
|
535
|
-
agent_id =
|
|
618
|
+
agent_id = (
|
|
619
|
+
agent_link
|
|
620
|
+
if isinstance(agent_link, str)
|
|
621
|
+
else agent_link.get("agent_id")
|
|
622
|
+
)
|
|
536
623
|
# Ensure agent_id and user_id are strings for JSON serialization
|
|
537
624
|
agent_id_str = str(agent_id)
|
|
538
625
|
user_id_str = str(result.registration["user_id"])
|
|
539
626
|
|
|
540
|
-
agent_input = {
|
|
541
|
-
"messages": [
|
|
542
|
-
{"role": "human", "content": message}
|
|
543
|
-
]
|
|
544
|
-
}
|
|
627
|
+
agent_input = {"messages": [{"role": "human", "content": message}]}
|
|
545
628
|
|
|
546
629
|
try:
|
|
547
630
|
success = await self._invoke_agent(
|
|
@@ -552,45 +635,46 @@ class TriggerServer:
|
|
|
552
635
|
if success:
|
|
553
636
|
agents_invoked += 1
|
|
554
637
|
except Exception as e:
|
|
555
|
-
logger.error(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
638
|
+
logger.error(
|
|
639
|
+
f"Error invoking agent {agent_id_str}: {e}", exc_info=True
|
|
640
|
+
)
|
|
641
|
+
logger.info(
|
|
642
|
+
f"Processed trigger handler with {len(result.agent_messages)} messages, invoked {agents_invoked} agents"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return {"success": True, "agents_invoked": agents_invoked}
|
|
646
|
+
|
|
563
647
|
except HTTPException:
|
|
564
648
|
raise
|
|
565
649
|
except Exception as e:
|
|
566
650
|
logger.error(f"Error in trigger handler: {e}", exc_info=True)
|
|
567
651
|
raise HTTPException(
|
|
568
|
-
status_code=500,
|
|
569
|
-
detail=f"Trigger processing failed: {str(e)}"
|
|
652
|
+
status_code=500, detail=f"Trigger processing failed: {str(e)}"
|
|
570
653
|
)
|
|
571
|
-
|
|
572
|
-
|
|
654
|
+
|
|
573
655
|
async def _invoke_agent(
|
|
574
656
|
self,
|
|
575
657
|
agent_id: str,
|
|
576
658
|
user_id: str,
|
|
577
|
-
input_data:
|
|
659
|
+
input_data: dict[str, Any],
|
|
578
660
|
) -> bool:
|
|
579
661
|
"""Invoke LangGraph agent using the SDK."""
|
|
580
662
|
# Ensure user_id is a string for JSON serialization
|
|
581
663
|
user_id_str = str(user_id)
|
|
582
664
|
logger.info(f"Invoking LangGraph agent {agent_id} for user {user_id_str}")
|
|
583
|
-
|
|
665
|
+
|
|
584
666
|
try:
|
|
585
667
|
headers = {
|
|
586
668
|
"x-auth-scheme": "agent-builder-trigger",
|
|
587
669
|
"x-user-id": user_id_str,
|
|
588
670
|
}
|
|
589
|
-
|
|
671
|
+
|
|
590
672
|
# Note: API key is already set in client initialization, no need to add to headers
|
|
591
673
|
if not self.langsmith_api_key:
|
|
592
|
-
logger.warning(
|
|
593
|
-
|
|
674
|
+
logger.warning(
|
|
675
|
+
"No LANGSMITH_API_KEY available - authentication may fail"
|
|
676
|
+
)
|
|
677
|
+
|
|
594
678
|
thread = await self.langgraph_client.threads.create(
|
|
595
679
|
metadata={
|
|
596
680
|
"triggered_by": "langchain-triggers",
|
|
@@ -601,7 +685,7 @@ class TriggerServer:
|
|
|
601
685
|
logger.info(f"Created thread {thread['thread_id']} for agent {agent_id}")
|
|
602
686
|
|
|
603
687
|
run = await self.langgraph_client.runs.create(
|
|
604
|
-
thread_id=thread[
|
|
688
|
+
thread_id=thread["thread_id"],
|
|
605
689
|
assistant_id=agent_id,
|
|
606
690
|
input=input_data,
|
|
607
691
|
metadata={
|
|
@@ -610,35 +694,39 @@ class TriggerServer:
|
|
|
610
694
|
},
|
|
611
695
|
headers=headers,
|
|
612
696
|
)
|
|
613
|
-
|
|
614
|
-
logger.info(
|
|
697
|
+
|
|
698
|
+
logger.info(
|
|
699
|
+
f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}"
|
|
700
|
+
)
|
|
615
701
|
return True
|
|
616
|
-
|
|
702
|
+
|
|
617
703
|
except Exception as e:
|
|
618
704
|
# Handle 404s (agent not found) as warnings, not errors
|
|
619
|
-
if
|
|
620
|
-
|
|
705
|
+
if (
|
|
706
|
+
hasattr(e, "response")
|
|
707
|
+
and getattr(e.response, "status_code", None) == 404
|
|
708
|
+
):
|
|
709
|
+
logger.warning(
|
|
710
|
+
f"Agent {agent_id} not found (404) - agent may have been deleted or moved"
|
|
711
|
+
)
|
|
621
712
|
return False
|
|
622
713
|
else:
|
|
623
714
|
logger.error(f"Error invoking agent {agent_id}: {e}")
|
|
624
715
|
raise
|
|
625
|
-
|
|
716
|
+
|
|
626
717
|
def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
|
|
627
718
|
"""Check if a trigger is from Slack and requires HMAC signature verification."""
|
|
628
|
-
return (
|
|
629
|
-
|
|
630
|
-
"slack" in trigger.id.lower()
|
|
631
|
-
)
|
|
632
|
-
|
|
719
|
+
return trigger.provider.lower() == "slack" or "slack" in trigger.id.lower()
|
|
720
|
+
|
|
633
721
|
async def _verify_slack_webhook_auth(self, request: Request) -> None:
|
|
634
722
|
"""Verify Slack HMAC signature for webhook requests.
|
|
635
|
-
|
|
723
|
+
|
|
636
724
|
Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
|
|
637
725
|
The signature is computed from the timestamp, body, and signing secret.
|
|
638
|
-
|
|
726
|
+
|
|
639
727
|
Args:
|
|
640
728
|
request: The FastAPI request object
|
|
641
|
-
|
|
729
|
+
|
|
642
730
|
Raises:
|
|
643
731
|
HTTPException: If authentication fails
|
|
644
732
|
"""
|
|
@@ -648,67 +736,70 @@ class TriggerServer:
|
|
|
648
736
|
logger.error("SLACK_SIGNING_SECRET environment variable not set")
|
|
649
737
|
raise HTTPException(
|
|
650
738
|
status_code=500,
|
|
651
|
-
detail="Slack signing secret not configured on server"
|
|
739
|
+
detail="Slack signing secret not configured on server",
|
|
652
740
|
)
|
|
653
|
-
|
|
741
|
+
|
|
654
742
|
headers_dict = dict(request.headers)
|
|
655
743
|
signature, timestamp = extract_slack_headers(headers_dict)
|
|
656
|
-
|
|
744
|
+
|
|
657
745
|
if not signature:
|
|
658
746
|
logger.error("Missing X-Slack-Signature header")
|
|
659
747
|
raise HTTPException(
|
|
660
748
|
status_code=401,
|
|
661
|
-
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
|
|
749
|
+
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
|
|
662
750
|
)
|
|
663
|
-
|
|
751
|
+
|
|
664
752
|
if not timestamp:
|
|
665
753
|
logger.error("Missing X-Slack-Request-Timestamp header")
|
|
666
754
|
raise HTTPException(
|
|
667
755
|
status_code=401,
|
|
668
|
-
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
|
|
756
|
+
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
|
|
669
757
|
)
|
|
670
|
-
|
|
758
|
+
|
|
671
759
|
body = await request.body()
|
|
672
|
-
body_str = body.decode(
|
|
673
|
-
|
|
760
|
+
body_str = body.decode("utf-8")
|
|
761
|
+
|
|
674
762
|
try:
|
|
675
763
|
verify_slack_signature(
|
|
676
764
|
signing_secret=signing_secret,
|
|
677
765
|
timestamp=timestamp,
|
|
678
766
|
body=body_str,
|
|
679
|
-
signature=signature
|
|
767
|
+
signature=signature,
|
|
768
|
+
)
|
|
769
|
+
logger.info(
|
|
770
|
+
f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
|
|
680
771
|
)
|
|
681
|
-
logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
|
|
682
772
|
except SlackSignatureVerificationError as e:
|
|
683
773
|
logger.error(f"Slack signature verification failed: {e}")
|
|
684
774
|
raise HTTPException(
|
|
685
775
|
status_code=401,
|
|
686
|
-
detail=f"Slack signature verification failed: {str(e)}"
|
|
776
|
+
detail=f"Slack signature verification failed: {str(e)}",
|
|
687
777
|
)
|
|
688
|
-
|
|
778
|
+
|
|
689
779
|
# Store verification info in request state
|
|
690
780
|
request.state.slack_verified = True
|
|
691
781
|
request.state.slack_timestamp = timestamp
|
|
692
|
-
|
|
782
|
+
|
|
693
783
|
except HTTPException:
|
|
694
784
|
raise
|
|
695
785
|
except Exception as e:
|
|
696
786
|
logger.error(f"Unexpected error during Slack webhook authentication: {e}")
|
|
697
787
|
raise HTTPException(
|
|
698
|
-
status_code=500,
|
|
699
|
-
detail=f"Authentication error: {str(e)}"
|
|
788
|
+
status_code=500, detail=f"Authentication error: {str(e)}"
|
|
700
789
|
)
|
|
701
|
-
|
|
702
|
-
async def _verify_slack_webhook_auth_with_body(
|
|
790
|
+
|
|
791
|
+
async def _verify_slack_webhook_auth_with_body(
|
|
792
|
+
self, request: Request, body_str: str
|
|
793
|
+
) -> None:
|
|
703
794
|
"""Verify Slack HMAC signature for webhook requests using pre-read body.
|
|
704
|
-
|
|
795
|
+
|
|
705
796
|
Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
|
|
706
797
|
The signature is computed from the timestamp, body, and signing secret.
|
|
707
|
-
|
|
798
|
+
|
|
708
799
|
Args:
|
|
709
800
|
request: The FastAPI request object
|
|
710
801
|
body_str: The request body as a string (already read)
|
|
711
|
-
|
|
802
|
+
|
|
712
803
|
Raises:
|
|
713
804
|
HTTPException: If authentication fails
|
|
714
805
|
"""
|
|
@@ -718,54 +809,55 @@ class TriggerServer:
|
|
|
718
809
|
logger.error("SLACK_SIGNING_SECRET environment variable not set")
|
|
719
810
|
raise HTTPException(
|
|
720
811
|
status_code=500,
|
|
721
|
-
detail="Slack signing secret not configured on server"
|
|
812
|
+
detail="Slack signing secret not configured on server",
|
|
722
813
|
)
|
|
723
|
-
|
|
814
|
+
|
|
724
815
|
headers_dict = dict(request.headers)
|
|
725
816
|
signature, timestamp = extract_slack_headers(headers_dict)
|
|
726
|
-
|
|
817
|
+
|
|
727
818
|
if not signature:
|
|
728
819
|
logger.error("Missing X-Slack-Signature header")
|
|
729
820
|
raise HTTPException(
|
|
730
821
|
status_code=401,
|
|
731
|
-
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
|
|
822
|
+
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
|
|
732
823
|
)
|
|
733
|
-
|
|
824
|
+
|
|
734
825
|
if not timestamp:
|
|
735
826
|
logger.error("Missing X-Slack-Request-Timestamp header")
|
|
736
827
|
raise HTTPException(
|
|
737
828
|
status_code=401,
|
|
738
|
-
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
|
|
829
|
+
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
|
|
739
830
|
)
|
|
740
|
-
|
|
831
|
+
|
|
741
832
|
try:
|
|
742
833
|
verify_slack_signature(
|
|
743
834
|
signing_secret=signing_secret,
|
|
744
835
|
timestamp=timestamp,
|
|
745
836
|
body=body_str,
|
|
746
|
-
signature=signature
|
|
837
|
+
signature=signature,
|
|
838
|
+
)
|
|
839
|
+
logger.info(
|
|
840
|
+
f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
|
|
747
841
|
)
|
|
748
|
-
logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
|
|
749
842
|
except SlackSignatureVerificationError as e:
|
|
750
843
|
logger.error(f"Slack signature verification failed: {e}")
|
|
751
844
|
raise HTTPException(
|
|
752
845
|
status_code=401,
|
|
753
|
-
detail=f"Slack signature verification failed: {str(e)}"
|
|
846
|
+
detail=f"Slack signature verification failed: {str(e)}",
|
|
754
847
|
)
|
|
755
|
-
|
|
848
|
+
|
|
756
849
|
# Store verification info in request state
|
|
757
850
|
request.state.slack_verified = True
|
|
758
851
|
request.state.slack_timestamp = timestamp
|
|
759
|
-
|
|
852
|
+
|
|
760
853
|
except HTTPException:
|
|
761
854
|
raise
|
|
762
855
|
except Exception as e:
|
|
763
856
|
logger.error(f"Unexpected error during Slack webhook authentication: {e}")
|
|
764
857
|
raise HTTPException(
|
|
765
|
-
status_code=500,
|
|
766
|
-
detail=f"Authentication error: {str(e)}"
|
|
858
|
+
status_code=500, detail=f"Authentication error: {str(e)}"
|
|
767
859
|
)
|
|
768
|
-
|
|
860
|
+
|
|
769
861
|
def get_app(self) -> FastAPI:
|
|
770
862
|
"""Get the FastAPI app instance."""
|
|
771
863
|
return self.app
|