langchain-trigger-server 0.2.6rc8__py3-none-any.whl → 0.2.8__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.8.dist-info}/METADATA +4 -5
- langchain_trigger_server-0.2.8.dist-info/RECORD +15 -0
- langchain_triggers/__init__.py +8 -3
- langchain_triggers/app.py +351 -253
- langchain_triggers/auth/__init__.py +3 -4
- langchain_triggers/auth/slack_hmac.py +21 -26
- langchain_triggers/core.py +58 -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.8.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,132 @@ 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
|
+
)
|
|
326
|
+
client_metadata = payload.pop("metadata", None)
|
|
296
327
|
|
|
297
328
|
# Parse payload into registration model first
|
|
298
329
|
try:
|
|
299
330
|
registration_instance = trigger.registration_model(**payload)
|
|
300
331
|
except Exception as e:
|
|
301
332
|
raise HTTPException(
|
|
302
|
-
status_code=400,
|
|
303
|
-
detail=f"Invalid payload for trigger: {str(e)}"
|
|
333
|
+
status_code=400, detail=f"Invalid payload for trigger: {str(e)}"
|
|
304
334
|
)
|
|
305
|
-
|
|
335
|
+
|
|
306
336
|
# Check for duplicate registration based on resource data within this user's scope
|
|
307
337
|
resource_dict = registration_instance.model_dump()
|
|
308
|
-
existing_registration =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
338
|
+
existing_registration = (
|
|
339
|
+
await self.database.find_user_registration_by_resource(
|
|
340
|
+
user_id=user_id,
|
|
341
|
+
template_id=trigger.id,
|
|
342
|
+
resource_data=resource_dict,
|
|
343
|
+
)
|
|
312
344
|
)
|
|
313
345
|
|
|
314
346
|
if existing_registration:
|
|
315
347
|
raise HTTPException(
|
|
316
348
|
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')}"
|
|
349
|
+
detail=f"You already have a registration with this configuration for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}",
|
|
318
350
|
)
|
|
319
|
-
result = await trigger.registration_handler(
|
|
320
|
-
|
|
351
|
+
result = await trigger.registration_handler(
|
|
352
|
+
user_id, self.langchain_auth_client, registration_instance
|
|
353
|
+
)
|
|
354
|
+
|
|
321
355
|
# Check if handler requested to skip registration (e.g., for OAuth or URL verification)
|
|
322
356
|
if not result.create_registration:
|
|
323
|
-
logger.info(
|
|
324
|
-
|
|
357
|
+
logger.info(
|
|
358
|
+
"Registration handler requested to skip database creation"
|
|
359
|
+
)
|
|
325
360
|
import json
|
|
361
|
+
|
|
362
|
+
from fastapi import Response
|
|
363
|
+
|
|
326
364
|
return Response(
|
|
327
365
|
content=json.dumps(result.response_body),
|
|
328
366
|
status_code=result.status_code,
|
|
329
|
-
media_type="application/json"
|
|
367
|
+
media_type="application/json",
|
|
330
368
|
)
|
|
331
|
-
|
|
369
|
+
|
|
332
370
|
resource_dict = registration_instance.model_dump()
|
|
333
371
|
|
|
372
|
+
merged_metadata = {}
|
|
373
|
+
if client_metadata:
|
|
374
|
+
merged_metadata["client_metadata"] = client_metadata
|
|
375
|
+
merged_metadata.update(result.metadata)
|
|
376
|
+
|
|
334
377
|
registration = await self.database.create_trigger_registration(
|
|
335
378
|
user_id=user_id,
|
|
336
379
|
template_id=trigger.id,
|
|
337
380
|
resource=resource_dict,
|
|
338
|
-
metadata=
|
|
381
|
+
metadata=merged_metadata,
|
|
339
382
|
)
|
|
340
|
-
|
|
383
|
+
|
|
341
384
|
if not registration:
|
|
342
|
-
raise HTTPException(
|
|
343
|
-
|
|
385
|
+
raise HTTPException(
|
|
386
|
+
status_code=500, detail="Failed to create trigger registration"
|
|
387
|
+
)
|
|
388
|
+
|
|
344
389
|
# Reload cron manager to pick up any new cron registrations
|
|
345
390
|
await self.cron_manager.reload_from_database()
|
|
346
|
-
|
|
391
|
+
|
|
347
392
|
# Return registration result
|
|
348
393
|
return {
|
|
349
394
|
"success": True,
|
|
350
395
|
"data": registration,
|
|
351
|
-
"metadata": result.metadata
|
|
396
|
+
"metadata": result.metadata,
|
|
352
397
|
}
|
|
353
|
-
|
|
398
|
+
|
|
354
399
|
except HTTPException:
|
|
355
400
|
raise
|
|
356
401
|
except Exception as e:
|
|
357
402
|
logger.exception(f"Error creating trigger registration: {e}")
|
|
358
403
|
raise HTTPException(status_code=500, detail=str(e))
|
|
359
|
-
|
|
404
|
+
|
|
360
405
|
@self.app.get("/v1/triggers/registrations/{registration_id}/agents")
|
|
361
|
-
async def api_list_registration_agents(
|
|
406
|
+
async def api_list_registration_agents(
|
|
407
|
+
registration_id: str,
|
|
408
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
409
|
+
) -> dict[str, Any]:
|
|
362
410
|
"""List agents linked to this registration."""
|
|
363
411
|
try:
|
|
364
412
|
user_id = current_user["identity"]
|
|
365
|
-
|
|
413
|
+
|
|
366
414
|
# Get the specific trigger registration
|
|
367
|
-
trigger = await self.database.get_trigger_registration(
|
|
415
|
+
trigger = await self.database.get_trigger_registration(
|
|
416
|
+
registration_id, user_id
|
|
417
|
+
)
|
|
368
418
|
if not trigger:
|
|
369
|
-
raise HTTPException(
|
|
370
|
-
|
|
419
|
+
raise HTTPException(
|
|
420
|
+
status_code=404,
|
|
421
|
+
detail="Trigger registration not found or access denied",
|
|
422
|
+
)
|
|
423
|
+
|
|
371
424
|
# Return the linked agent IDs
|
|
372
425
|
return {
|
|
373
426
|
"success": True,
|
|
374
|
-
"data": trigger.get("linked_assistant_ids", [])
|
|
427
|
+
"data": trigger.get("linked_assistant_ids", []),
|
|
375
428
|
}
|
|
376
|
-
|
|
429
|
+
|
|
377
430
|
except HTTPException:
|
|
378
431
|
raise
|
|
379
432
|
except Exception as e:
|
|
380
433
|
logger.error(f"Error getting registration agents: {e}")
|
|
381
434
|
raise HTTPException(status_code=500, detail=str(e))
|
|
382
|
-
|
|
435
|
+
|
|
383
436
|
@self.app.post("/v1/triggers/registrations/{registration_id}/agents/{agent_id}")
|
|
384
|
-
async def api_add_agent_to_trigger(
|
|
437
|
+
async def api_add_agent_to_trigger(
|
|
438
|
+
registration_id: str,
|
|
439
|
+
agent_id: str,
|
|
440
|
+
request: Request,
|
|
441
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
442
|
+
) -> dict[str, Any]:
|
|
385
443
|
"""Add an agent to a trigger registration."""
|
|
386
444
|
try:
|
|
387
445
|
# Parse request body for field selection
|
|
@@ -390,67 +448,80 @@ class TriggerServer:
|
|
|
390
448
|
field_selection = body.get("field_selection")
|
|
391
449
|
except:
|
|
392
450
|
field_selection = None
|
|
393
|
-
|
|
451
|
+
|
|
394
452
|
user_id = current_user["identity"]
|
|
395
|
-
|
|
453
|
+
|
|
396
454
|
# Verify the trigger registration exists and belongs to the user
|
|
397
|
-
registration = await self.database.get_trigger_registration(
|
|
455
|
+
registration = await self.database.get_trigger_registration(
|
|
456
|
+
registration_id, user_id
|
|
457
|
+
)
|
|
398
458
|
if not registration:
|
|
399
|
-
raise HTTPException(
|
|
400
|
-
|
|
459
|
+
raise HTTPException(
|
|
460
|
+
status_code=404,
|
|
461
|
+
detail="Trigger registration not found or access denied",
|
|
462
|
+
)
|
|
463
|
+
|
|
401
464
|
# Link the agent to the trigger
|
|
402
465
|
success = await self.database.link_agent_to_trigger(
|
|
403
466
|
agent_id=agent_id,
|
|
404
467
|
registration_id=registration_id,
|
|
405
468
|
created_by=user_id,
|
|
406
|
-
field_selection=field_selection
|
|
469
|
+
field_selection=field_selection,
|
|
407
470
|
)
|
|
408
|
-
|
|
471
|
+
|
|
409
472
|
if not success:
|
|
410
|
-
raise HTTPException(
|
|
411
|
-
|
|
473
|
+
raise HTTPException(
|
|
474
|
+
status_code=500, detail="Failed to link agent to trigger"
|
|
475
|
+
)
|
|
476
|
+
|
|
412
477
|
return {
|
|
413
478
|
"success": True,
|
|
414
479
|
"message": f"Successfully linked agent {agent_id} to trigger {registration_id}",
|
|
415
|
-
"data": {
|
|
416
|
-
"registration_id": registration_id,
|
|
417
|
-
"agent_id": agent_id
|
|
418
|
-
}
|
|
480
|
+
"data": {"registration_id": registration_id, "agent_id": agent_id},
|
|
419
481
|
}
|
|
420
|
-
|
|
482
|
+
|
|
421
483
|
except HTTPException:
|
|
422
484
|
raise
|
|
423
485
|
except Exception as e:
|
|
424
486
|
logger.error(f"Error linking agent to trigger: {e}")
|
|
425
487
|
raise HTTPException(status_code=500, detail=str(e))
|
|
426
|
-
|
|
427
|
-
@self.app.delete(
|
|
428
|
-
|
|
488
|
+
|
|
489
|
+
@self.app.delete(
|
|
490
|
+
"/v1/triggers/registrations/{registration_id}/agents/{agent_id}"
|
|
491
|
+
)
|
|
492
|
+
async def api_remove_agent_from_trigger(
|
|
493
|
+
registration_id: str,
|
|
494
|
+
agent_id: str,
|
|
495
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
496
|
+
) -> dict[str, Any]:
|
|
429
497
|
"""Remove an agent from a trigger registration."""
|
|
430
498
|
try:
|
|
431
499
|
user_id = current_user["identity"]
|
|
432
500
|
|
|
433
501
|
# Verify the trigger registration exists and belongs to the user
|
|
434
|
-
registration = await self.database.get_trigger_registration(
|
|
502
|
+
registration = await self.database.get_trigger_registration(
|
|
503
|
+
registration_id, user_id
|
|
504
|
+
)
|
|
435
505
|
if not registration:
|
|
436
|
-
raise HTTPException(
|
|
506
|
+
raise HTTPException(
|
|
507
|
+
status_code=404,
|
|
508
|
+
detail="Trigger registration not found or access denied",
|
|
509
|
+
)
|
|
437
510
|
|
|
438
511
|
# Unlink the agent from the trigger
|
|
439
512
|
success = await self.database.unlink_agent_from_trigger(
|
|
440
|
-
agent_id=agent_id,
|
|
441
|
-
registration_id=registration_id
|
|
513
|
+
agent_id=agent_id, registration_id=registration_id
|
|
442
514
|
)
|
|
443
515
|
|
|
444
516
|
if not success:
|
|
445
|
-
raise HTTPException(
|
|
517
|
+
raise HTTPException(
|
|
518
|
+
status_code=500, detail="Failed to unlink agent from trigger"
|
|
519
|
+
)
|
|
446
520
|
|
|
447
521
|
return {
|
|
448
522
|
"success": True,
|
|
449
523
|
"message": f"Successfully unlinked agent {agent_id} from trigger {registration_id}",
|
|
450
|
-
"data": {
|
|
451
|
-
"registration_id": registration_id,
|
|
452
|
-
"agent_id": agent_id
|
|
453
|
-
}
|
|
524
|
+
"data": {"registration_id": registration_id, "agent_id": agent_id},
|
|
454
525
|
}
|
|
455
526
|
|
|
456
527
|
except HTTPException:
|
|
@@ -460,20 +531,31 @@ class TriggerServer:
|
|
|
460
531
|
raise HTTPException(status_code=500, detail=str(e))
|
|
461
532
|
|
|
462
533
|
@self.app.post("/v1/triggers/registrations/{registration_id}/execute")
|
|
463
|
-
async def api_execute_trigger_now(
|
|
534
|
+
async def api_execute_trigger_now(
|
|
535
|
+
registration_id: str,
|
|
536
|
+
current_user: dict[str, Any] = Depends(get_current_user),
|
|
537
|
+
) -> dict[str, Any]:
|
|
464
538
|
"""Manually execute a cron trigger registration immediately."""
|
|
465
539
|
try:
|
|
466
540
|
user_id = current_user["identity"]
|
|
467
541
|
|
|
468
542
|
# Verify the trigger registration exists and belongs to the user
|
|
469
|
-
registration = await self.database.get_trigger_registration(
|
|
543
|
+
registration = await self.database.get_trigger_registration(
|
|
544
|
+
registration_id, user_id
|
|
545
|
+
)
|
|
470
546
|
if not registration:
|
|
471
|
-
raise HTTPException(
|
|
547
|
+
raise HTTPException(
|
|
548
|
+
status_code=404,
|
|
549
|
+
detail="Trigger registration not found or access denied",
|
|
550
|
+
)
|
|
472
551
|
|
|
473
552
|
# Get the template to check if it's a cron trigger
|
|
474
553
|
template_id = registration.get("template_id")
|
|
475
554
|
if template_id != CRON_TRIGGER_ID:
|
|
476
|
-
raise HTTPException(
|
|
555
|
+
raise HTTPException(
|
|
556
|
+
status_code=400,
|
|
557
|
+
detail="Manual execution is only supported for cron triggers",
|
|
558
|
+
)
|
|
477
559
|
|
|
478
560
|
# Execute the cron trigger using the cron manager
|
|
479
561
|
agents_invoked = await self.cron_manager.execute_cron_job(registration)
|
|
@@ -481,7 +563,7 @@ class TriggerServer:
|
|
|
481
563
|
return {
|
|
482
564
|
"success": True,
|
|
483
565
|
"message": f"Manually executed cron trigger {registration_id}",
|
|
484
|
-
"agents_invoked": agents_invoked
|
|
566
|
+
"agents_invoked": agents_invoked,
|
|
485
567
|
}
|
|
486
568
|
|
|
487
569
|
except HTTPException:
|
|
@@ -489,29 +571,34 @@ class TriggerServer:
|
|
|
489
571
|
except Exception as e:
|
|
490
572
|
logger.error(f"Error executing trigger: {e}")
|
|
491
573
|
raise HTTPException(status_code=500, detail=str(e))
|
|
492
|
-
|
|
493
|
-
|
|
574
|
+
|
|
494
575
|
async def _handle_request(
|
|
495
|
-
self,
|
|
496
|
-
|
|
497
|
-
request: Request
|
|
498
|
-
) -> Dict[str, Any]:
|
|
576
|
+
self, trigger: TriggerTemplate, request: Request
|
|
577
|
+
) -> dict[str, Any]:
|
|
499
578
|
"""Handle an incoming request with a handler function."""
|
|
500
579
|
try:
|
|
501
580
|
if request.method == "POST":
|
|
502
|
-
if request.headers.get("content-type", "").startswith(
|
|
581
|
+
if request.headers.get("content-type", "").startswith(
|
|
582
|
+
"application/json"
|
|
583
|
+
):
|
|
503
584
|
# Read body once for both auth and parsing
|
|
504
585
|
body_bytes = await request.body()
|
|
505
586
|
body_str = body_bytes.decode("utf-8")
|
|
506
|
-
|
|
587
|
+
|
|
507
588
|
if self._is_slack_trigger(trigger):
|
|
508
|
-
await self._verify_slack_webhook_auth_with_body(
|
|
509
|
-
|
|
589
|
+
await self._verify_slack_webhook_auth_with_body(
|
|
590
|
+
request, body_str
|
|
591
|
+
)
|
|
592
|
+
|
|
510
593
|
import json
|
|
594
|
+
|
|
511
595
|
payload = json.loads(body_str)
|
|
512
596
|
|
|
513
|
-
if
|
|
514
|
-
|
|
597
|
+
if (
|
|
598
|
+
payload.get("type") == "url_verification"
|
|
599
|
+
and "challenge" in payload
|
|
600
|
+
):
|
|
601
|
+
logger.info("Responding to Slack URL verification challenge")
|
|
515
602
|
return {"challenge": payload["challenge"]}
|
|
516
603
|
else:
|
|
517
604
|
# Handle form data or other content types
|
|
@@ -521,7 +608,9 @@ class TriggerServer:
|
|
|
521
608
|
payload = dict(request.query_params)
|
|
522
609
|
|
|
523
610
|
query_params = dict(request.query_params)
|
|
524
|
-
result = await trigger.trigger_handler(
|
|
611
|
+
result = await trigger.trigger_handler(
|
|
612
|
+
payload, query_params, self.database, self.langchain_auth_client
|
|
613
|
+
)
|
|
525
614
|
if not result.invoke_agent:
|
|
526
615
|
return result.response_body
|
|
527
616
|
|
|
@@ -532,16 +621,16 @@ class TriggerServer:
|
|
|
532
621
|
# Iterate through each message and invoke agents for each
|
|
533
622
|
for message in result.agent_messages:
|
|
534
623
|
for agent_link in agent_links:
|
|
535
|
-
agent_id =
|
|
624
|
+
agent_id = (
|
|
625
|
+
agent_link
|
|
626
|
+
if isinstance(agent_link, str)
|
|
627
|
+
else agent_link.get("agent_id")
|
|
628
|
+
)
|
|
536
629
|
# Ensure agent_id and user_id are strings for JSON serialization
|
|
537
630
|
agent_id_str = str(agent_id)
|
|
538
631
|
user_id_str = str(result.registration["user_id"])
|
|
539
632
|
|
|
540
|
-
agent_input = {
|
|
541
|
-
"messages": [
|
|
542
|
-
{"role": "human", "content": message}
|
|
543
|
-
]
|
|
544
|
-
}
|
|
633
|
+
agent_input = {"messages": [{"role": "human", "content": message}]}
|
|
545
634
|
|
|
546
635
|
try:
|
|
547
636
|
success = await self._invoke_agent(
|
|
@@ -552,45 +641,46 @@ class TriggerServer:
|
|
|
552
641
|
if success:
|
|
553
642
|
agents_invoked += 1
|
|
554
643
|
except Exception as e:
|
|
555
|
-
logger.error(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
644
|
+
logger.error(
|
|
645
|
+
f"Error invoking agent {agent_id_str}: {e}", exc_info=True
|
|
646
|
+
)
|
|
647
|
+
logger.info(
|
|
648
|
+
f"Processed trigger handler with {len(result.agent_messages)} messages, invoked {agents_invoked} agents"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
return {"success": True, "agents_invoked": agents_invoked}
|
|
652
|
+
|
|
563
653
|
except HTTPException:
|
|
564
654
|
raise
|
|
565
655
|
except Exception as e:
|
|
566
656
|
logger.error(f"Error in trigger handler: {e}", exc_info=True)
|
|
567
657
|
raise HTTPException(
|
|
568
|
-
status_code=500,
|
|
569
|
-
detail=f"Trigger processing failed: {str(e)}"
|
|
658
|
+
status_code=500, detail=f"Trigger processing failed: {str(e)}"
|
|
570
659
|
)
|
|
571
|
-
|
|
572
|
-
|
|
660
|
+
|
|
573
661
|
async def _invoke_agent(
|
|
574
662
|
self,
|
|
575
663
|
agent_id: str,
|
|
576
664
|
user_id: str,
|
|
577
|
-
input_data:
|
|
665
|
+
input_data: dict[str, Any],
|
|
578
666
|
) -> bool:
|
|
579
667
|
"""Invoke LangGraph agent using the SDK."""
|
|
580
668
|
# Ensure user_id is a string for JSON serialization
|
|
581
669
|
user_id_str = str(user_id)
|
|
582
670
|
logger.info(f"Invoking LangGraph agent {agent_id} for user {user_id_str}")
|
|
583
|
-
|
|
671
|
+
|
|
584
672
|
try:
|
|
585
673
|
headers = {
|
|
586
674
|
"x-auth-scheme": "agent-builder-trigger",
|
|
587
675
|
"x-user-id": user_id_str,
|
|
588
676
|
}
|
|
589
|
-
|
|
677
|
+
|
|
590
678
|
# Note: API key is already set in client initialization, no need to add to headers
|
|
591
679
|
if not self.langsmith_api_key:
|
|
592
|
-
logger.warning(
|
|
593
|
-
|
|
680
|
+
logger.warning(
|
|
681
|
+
"No LANGSMITH_API_KEY available - authentication may fail"
|
|
682
|
+
)
|
|
683
|
+
|
|
594
684
|
thread = await self.langgraph_client.threads.create(
|
|
595
685
|
metadata={
|
|
596
686
|
"triggered_by": "langchain-triggers",
|
|
@@ -601,7 +691,7 @@ class TriggerServer:
|
|
|
601
691
|
logger.info(f"Created thread {thread['thread_id']} for agent {agent_id}")
|
|
602
692
|
|
|
603
693
|
run = await self.langgraph_client.runs.create(
|
|
604
|
-
thread_id=thread[
|
|
694
|
+
thread_id=thread["thread_id"],
|
|
605
695
|
assistant_id=agent_id,
|
|
606
696
|
input=input_data,
|
|
607
697
|
metadata={
|
|
@@ -610,35 +700,39 @@ class TriggerServer:
|
|
|
610
700
|
},
|
|
611
701
|
headers=headers,
|
|
612
702
|
)
|
|
613
|
-
|
|
614
|
-
logger.info(
|
|
703
|
+
|
|
704
|
+
logger.info(
|
|
705
|
+
f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}"
|
|
706
|
+
)
|
|
615
707
|
return True
|
|
616
|
-
|
|
708
|
+
|
|
617
709
|
except Exception as e:
|
|
618
710
|
# Handle 404s (agent not found) as warnings, not errors
|
|
619
|
-
if
|
|
620
|
-
|
|
711
|
+
if (
|
|
712
|
+
hasattr(e, "response")
|
|
713
|
+
and getattr(e.response, "status_code", None) == 404
|
|
714
|
+
):
|
|
715
|
+
logger.warning(
|
|
716
|
+
f"Agent {agent_id} not found (404) - agent may have been deleted or moved"
|
|
717
|
+
)
|
|
621
718
|
return False
|
|
622
719
|
else:
|
|
623
720
|
logger.error(f"Error invoking agent {agent_id}: {e}")
|
|
624
721
|
raise
|
|
625
|
-
|
|
722
|
+
|
|
626
723
|
def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
|
|
627
724
|
"""Check if a trigger is from Slack and requires HMAC signature verification."""
|
|
628
|
-
return (
|
|
629
|
-
|
|
630
|
-
"slack" in trigger.id.lower()
|
|
631
|
-
)
|
|
632
|
-
|
|
725
|
+
return trigger.provider.lower() == "slack" or "slack" in trigger.id.lower()
|
|
726
|
+
|
|
633
727
|
async def _verify_slack_webhook_auth(self, request: Request) -> None:
|
|
634
728
|
"""Verify Slack HMAC signature for webhook requests.
|
|
635
|
-
|
|
729
|
+
|
|
636
730
|
Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
|
|
637
731
|
The signature is computed from the timestamp, body, and signing secret.
|
|
638
|
-
|
|
732
|
+
|
|
639
733
|
Args:
|
|
640
734
|
request: The FastAPI request object
|
|
641
|
-
|
|
735
|
+
|
|
642
736
|
Raises:
|
|
643
737
|
HTTPException: If authentication fails
|
|
644
738
|
"""
|
|
@@ -648,67 +742,70 @@ class TriggerServer:
|
|
|
648
742
|
logger.error("SLACK_SIGNING_SECRET environment variable not set")
|
|
649
743
|
raise HTTPException(
|
|
650
744
|
status_code=500,
|
|
651
|
-
detail="Slack signing secret not configured on server"
|
|
745
|
+
detail="Slack signing secret not configured on server",
|
|
652
746
|
)
|
|
653
|
-
|
|
747
|
+
|
|
654
748
|
headers_dict = dict(request.headers)
|
|
655
749
|
signature, timestamp = extract_slack_headers(headers_dict)
|
|
656
|
-
|
|
750
|
+
|
|
657
751
|
if not signature:
|
|
658
752
|
logger.error("Missing X-Slack-Signature header")
|
|
659
753
|
raise HTTPException(
|
|
660
754
|
status_code=401,
|
|
661
|
-
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
|
|
755
|
+
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
|
|
662
756
|
)
|
|
663
|
-
|
|
757
|
+
|
|
664
758
|
if not timestamp:
|
|
665
759
|
logger.error("Missing X-Slack-Request-Timestamp header")
|
|
666
760
|
raise HTTPException(
|
|
667
761
|
status_code=401,
|
|
668
|
-
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
|
|
762
|
+
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
|
|
669
763
|
)
|
|
670
|
-
|
|
764
|
+
|
|
671
765
|
body = await request.body()
|
|
672
|
-
body_str = body.decode(
|
|
673
|
-
|
|
766
|
+
body_str = body.decode("utf-8")
|
|
767
|
+
|
|
674
768
|
try:
|
|
675
769
|
verify_slack_signature(
|
|
676
770
|
signing_secret=signing_secret,
|
|
677
771
|
timestamp=timestamp,
|
|
678
772
|
body=body_str,
|
|
679
|
-
signature=signature
|
|
773
|
+
signature=signature,
|
|
774
|
+
)
|
|
775
|
+
logger.info(
|
|
776
|
+
f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
|
|
680
777
|
)
|
|
681
|
-
logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
|
|
682
778
|
except SlackSignatureVerificationError as e:
|
|
683
779
|
logger.error(f"Slack signature verification failed: {e}")
|
|
684
780
|
raise HTTPException(
|
|
685
781
|
status_code=401,
|
|
686
|
-
detail=f"Slack signature verification failed: {str(e)}"
|
|
782
|
+
detail=f"Slack signature verification failed: {str(e)}",
|
|
687
783
|
)
|
|
688
|
-
|
|
784
|
+
|
|
689
785
|
# Store verification info in request state
|
|
690
786
|
request.state.slack_verified = True
|
|
691
787
|
request.state.slack_timestamp = timestamp
|
|
692
|
-
|
|
788
|
+
|
|
693
789
|
except HTTPException:
|
|
694
790
|
raise
|
|
695
791
|
except Exception as e:
|
|
696
792
|
logger.error(f"Unexpected error during Slack webhook authentication: {e}")
|
|
697
793
|
raise HTTPException(
|
|
698
|
-
status_code=500,
|
|
699
|
-
detail=f"Authentication error: {str(e)}"
|
|
794
|
+
status_code=500, detail=f"Authentication error: {str(e)}"
|
|
700
795
|
)
|
|
701
|
-
|
|
702
|
-
async def _verify_slack_webhook_auth_with_body(
|
|
796
|
+
|
|
797
|
+
async def _verify_slack_webhook_auth_with_body(
|
|
798
|
+
self, request: Request, body_str: str
|
|
799
|
+
) -> None:
|
|
703
800
|
"""Verify Slack HMAC signature for webhook requests using pre-read body.
|
|
704
|
-
|
|
801
|
+
|
|
705
802
|
Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
|
|
706
803
|
The signature is computed from the timestamp, body, and signing secret.
|
|
707
|
-
|
|
804
|
+
|
|
708
805
|
Args:
|
|
709
806
|
request: The FastAPI request object
|
|
710
807
|
body_str: The request body as a string (already read)
|
|
711
|
-
|
|
808
|
+
|
|
712
809
|
Raises:
|
|
713
810
|
HTTPException: If authentication fails
|
|
714
811
|
"""
|
|
@@ -718,54 +815,55 @@ class TriggerServer:
|
|
|
718
815
|
logger.error("SLACK_SIGNING_SECRET environment variable not set")
|
|
719
816
|
raise HTTPException(
|
|
720
817
|
status_code=500,
|
|
721
|
-
detail="Slack signing secret not configured on server"
|
|
818
|
+
detail="Slack signing secret not configured on server",
|
|
722
819
|
)
|
|
723
|
-
|
|
820
|
+
|
|
724
821
|
headers_dict = dict(request.headers)
|
|
725
822
|
signature, timestamp = extract_slack_headers(headers_dict)
|
|
726
|
-
|
|
823
|
+
|
|
727
824
|
if not signature:
|
|
728
825
|
logger.error("Missing X-Slack-Signature header")
|
|
729
826
|
raise HTTPException(
|
|
730
827
|
status_code=401,
|
|
731
|
-
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
|
|
828
|
+
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification.",
|
|
732
829
|
)
|
|
733
|
-
|
|
830
|
+
|
|
734
831
|
if not timestamp:
|
|
735
832
|
logger.error("Missing X-Slack-Request-Timestamp header")
|
|
736
833
|
raise HTTPException(
|
|
737
834
|
status_code=401,
|
|
738
|
-
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
|
|
835
|
+
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp.",
|
|
739
836
|
)
|
|
740
|
-
|
|
837
|
+
|
|
741
838
|
try:
|
|
742
839
|
verify_slack_signature(
|
|
743
840
|
signing_secret=signing_secret,
|
|
744
841
|
timestamp=timestamp,
|
|
745
842
|
body=body_str,
|
|
746
|
-
signature=signature
|
|
843
|
+
signature=signature,
|
|
844
|
+
)
|
|
845
|
+
logger.info(
|
|
846
|
+
f"Successfully verified Slack webhook signature. Timestamp: {timestamp}"
|
|
747
847
|
)
|
|
748
|
-
logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
|
|
749
848
|
except SlackSignatureVerificationError as e:
|
|
750
849
|
logger.error(f"Slack signature verification failed: {e}")
|
|
751
850
|
raise HTTPException(
|
|
752
851
|
status_code=401,
|
|
753
|
-
detail=f"Slack signature verification failed: {str(e)}"
|
|
852
|
+
detail=f"Slack signature verification failed: {str(e)}",
|
|
754
853
|
)
|
|
755
|
-
|
|
854
|
+
|
|
756
855
|
# Store verification info in request state
|
|
757
856
|
request.state.slack_verified = True
|
|
758
857
|
request.state.slack_timestamp = timestamp
|
|
759
|
-
|
|
858
|
+
|
|
760
859
|
except HTTPException:
|
|
761
860
|
raise
|
|
762
861
|
except Exception as e:
|
|
763
862
|
logger.error(f"Unexpected error during Slack webhook authentication: {e}")
|
|
764
863
|
raise HTTPException(
|
|
765
|
-
status_code=500,
|
|
766
|
-
detail=f"Authentication error: {str(e)}"
|
|
864
|
+
status_code=500, detail=f"Authentication error: {str(e)}"
|
|
767
865
|
)
|
|
768
|
-
|
|
866
|
+
|
|
769
867
|
def get_app(self) -> FastAPI:
|
|
770
868
|
"""Get the FastAPI app instance."""
|
|
771
869
|
return self.app
|