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