langchain-trigger-server 0.2.6rc1__tar.gz → 0.2.6rc3__tar.gz
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.6rc1 → langchain_trigger_server-0.2.6rc3}/PKG-INFO +1 -1
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/app.py +97 -11
- langchain_trigger_server-0.2.6rc3/langchain_triggers/auth/__init__.py +16 -0
- langchain_trigger_server-0.2.6rc3/langchain_triggers/auth/slack_hmac.py +95 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/pyproject.toml +1 -1
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/.github/workflows/release.yml +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/.vscode/settings.json +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/README.md +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/__init__.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/core.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/cron_manager.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/database/__init__.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/database/interface.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/database/supabase.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/decorators.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/triggers/__init__.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/triggers/cron_trigger.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/test_framework.py +0 -0
- {langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-trigger-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6rc3
|
|
4
4
|
Summary: Generic event-driven triggers framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/langchain-ai/open-agent-platform
|
|
6
6
|
Project-URL: Repository, https://github.com/langchain-ai/open-agent-platform
|
{langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/app.py
RENAMED
|
@@ -16,6 +16,12 @@ from .decorators import TriggerTemplate
|
|
|
16
16
|
from .database import create_database, TriggerDatabaseInterface
|
|
17
17
|
from .cron_manager import CronTriggerManager
|
|
18
18
|
from .triggers.cron_trigger import CRON_TRIGGER_ID
|
|
19
|
+
from .auth.slack_hmac import (
|
|
20
|
+
verify_slack_signature,
|
|
21
|
+
get_slack_signing_secret,
|
|
22
|
+
extract_slack_headers,
|
|
23
|
+
SlackSignatureVerificationError,
|
|
24
|
+
)
|
|
19
25
|
|
|
20
26
|
logger = logging.getLogger(__name__)
|
|
21
27
|
|
|
@@ -29,7 +35,7 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
29
35
|
|
|
30
36
|
async def dispatch(self, request: Request, call_next):
|
|
31
37
|
# Skip auth for webhooks, health/root endpoints, and OPTIONS requests
|
|
32
|
-
if (request.url.path.startswith("/webhooks/") or
|
|
38
|
+
if (request.url.path.startswith("/v1/triggers/webhooks/") or
|
|
33
39
|
request.url.path in ["/", "/health"] or
|
|
34
40
|
request.method == "OPTIONS"):
|
|
35
41
|
return await call_next(request)
|
|
@@ -174,7 +180,7 @@ class TriggerServer:
|
|
|
174
180
|
async def handler_endpoint(request: Request) -> Dict[str, Any]:
|
|
175
181
|
return await self._handle_request(trigger, request)
|
|
176
182
|
|
|
177
|
-
handler_path = f"/webhooks/{trigger.id}"
|
|
183
|
+
handler_path = f"/v1/triggers/webhooks/{trigger.id}"
|
|
178
184
|
self.app.post(handler_path)(handler_endpoint)
|
|
179
185
|
logger.info(f"Added handler route: POST {handler_path}")
|
|
180
186
|
|
|
@@ -213,7 +219,7 @@ class TriggerServer:
|
|
|
213
219
|
async def health() -> Dict[str, str]:
|
|
214
220
|
return {"status": "healthy"}
|
|
215
221
|
|
|
216
|
-
@self.app.get("/
|
|
222
|
+
@self.app.get("/v1/triggers")
|
|
217
223
|
async def api_list_triggers() -> Dict[str, Any]:
|
|
218
224
|
"""List available trigger templates."""
|
|
219
225
|
templates = await self.database.get_trigger_templates()
|
|
@@ -224,7 +230,7 @@ class TriggerServer:
|
|
|
224
230
|
"provider": template["provider"],
|
|
225
231
|
"displayName": template["name"],
|
|
226
232
|
"description": template["description"],
|
|
227
|
-
"path": "/
|
|
233
|
+
"path": "/v1/triggers/registrations",
|
|
228
234
|
"method": "POST",
|
|
229
235
|
"payloadSchema": template.get("registration_schema", {}),
|
|
230
236
|
})
|
|
@@ -234,7 +240,7 @@ class TriggerServer:
|
|
|
234
240
|
"data": trigger_list
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
@self.app.get("/
|
|
243
|
+
@self.app.get("/v1/triggers/registrations")
|
|
238
244
|
async def api_list_registrations(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
239
245
|
"""List user's trigger registrations (user-scoped)."""
|
|
240
246
|
try:
|
|
@@ -266,7 +272,7 @@ class TriggerServer:
|
|
|
266
272
|
logger.error(f"Error listing registrations: {e}")
|
|
267
273
|
raise HTTPException(status_code=500, detail=str(e))
|
|
268
274
|
|
|
269
|
-
@self.app.post("/
|
|
275
|
+
@self.app.post("/v1/triggers/registrations")
|
|
270
276
|
async def api_create_registration(request: Request, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
271
277
|
"""Create a new trigger registration."""
|
|
272
278
|
try:
|
|
@@ -345,14 +351,14 @@ class TriggerServer:
|
|
|
345
351
|
logger.exception(f"Error creating trigger registration: {e}")
|
|
346
352
|
raise HTTPException(status_code=500, detail=str(e))
|
|
347
353
|
|
|
348
|
-
@self.app.get("/
|
|
354
|
+
@self.app.get("/v1/triggers/registrations/{registration_id}/agents")
|
|
349
355
|
async def api_list_registration_agents(registration_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
350
356
|
"""List agents linked to this registration."""
|
|
351
357
|
try:
|
|
352
358
|
user_id = current_user["identity"]
|
|
353
359
|
|
|
354
360
|
# Get the specific trigger registration
|
|
355
|
-
trigger = await self.database.
|
|
361
|
+
trigger = await self.database.get_trigger_registration(registration_id, user_id)
|
|
356
362
|
if not trigger:
|
|
357
363
|
raise HTTPException(status_code=404, detail="Trigger registration not found or access denied")
|
|
358
364
|
|
|
@@ -368,7 +374,7 @@ class TriggerServer:
|
|
|
368
374
|
logger.error(f"Error getting registration agents: {e}")
|
|
369
375
|
raise HTTPException(status_code=500, detail=str(e))
|
|
370
376
|
|
|
371
|
-
@self.app.post("/
|
|
377
|
+
@self.app.post("/v1/triggers/registrations/{registration_id}/agents/{agent_id}")
|
|
372
378
|
async def api_add_agent_to_trigger(registration_id: str, agent_id: str, request: Request, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
373
379
|
"""Add an agent to a trigger registration."""
|
|
374
380
|
try:
|
|
@@ -412,7 +418,7 @@ class TriggerServer:
|
|
|
412
418
|
logger.error(f"Error linking agent to trigger: {e}")
|
|
413
419
|
raise HTTPException(status_code=500, detail=str(e))
|
|
414
420
|
|
|
415
|
-
@self.app.delete("/
|
|
421
|
+
@self.app.delete("/v1/triggers/registrations/{registration_id}/agents/{agent_id}")
|
|
416
422
|
async def api_remove_agent_from_trigger(registration_id: str, agent_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
417
423
|
"""Remove an agent from a trigger registration."""
|
|
418
424
|
try:
|
|
@@ -447,7 +453,7 @@ class TriggerServer:
|
|
|
447
453
|
logger.error(f"Error unlinking agent from trigger: {e}")
|
|
448
454
|
raise HTTPException(status_code=500, detail=str(e))
|
|
449
455
|
|
|
450
|
-
@self.app.post("/
|
|
456
|
+
@self.app.post("/v1/triggers/registrations/{registration_id}/execute")
|
|
451
457
|
async def api_execute_trigger_now(registration_id: str, current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
|
452
458
|
"""Manually execute a cron trigger registration immediately."""
|
|
453
459
|
try:
|
|
@@ -486,6 +492,10 @@ class TriggerServer:
|
|
|
486
492
|
) -> Dict[str, Any]:
|
|
487
493
|
"""Handle an incoming request with a handler function."""
|
|
488
494
|
try:
|
|
495
|
+
# Slack webhook authentication
|
|
496
|
+
# Check if this is a Slack trigger that requires HMAC signature verification
|
|
497
|
+
if self._is_slack_trigger(trigger):
|
|
498
|
+
await self._verify_slack_webhook_auth(request)
|
|
489
499
|
|
|
490
500
|
# Parse request data
|
|
491
501
|
if request.method == "POST":
|
|
@@ -592,6 +602,82 @@ class TriggerServer:
|
|
|
592
602
|
logger.error(f"Error invoking agent {agent_id}: {e}")
|
|
593
603
|
raise
|
|
594
604
|
|
|
605
|
+
def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
|
|
606
|
+
"""Check if a trigger is from Slack and requires HMAC signature verification."""
|
|
607
|
+
return (
|
|
608
|
+
trigger.provider.lower() == "slack" or
|
|
609
|
+
"slack" in trigger.id.lower()
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
async def _verify_slack_webhook_auth(self, request: Request) -> None:
|
|
613
|
+
"""Verify Slack HMAC signature for webhook requests.
|
|
614
|
+
|
|
615
|
+
Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
|
|
616
|
+
The signature is computed from the timestamp, body, and signing secret.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
request: The FastAPI request object
|
|
620
|
+
|
|
621
|
+
Raises:
|
|
622
|
+
HTTPException: If authentication fails
|
|
623
|
+
"""
|
|
624
|
+
try:
|
|
625
|
+
signing_secret = get_slack_signing_secret()
|
|
626
|
+
if not signing_secret:
|
|
627
|
+
logger.error("SLACK_SIGNING_SECRET environment variable not set")
|
|
628
|
+
raise HTTPException(
|
|
629
|
+
status_code=500,
|
|
630
|
+
detail="Slack signing secret not configured on server"
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
headers_dict = dict(request.headers)
|
|
634
|
+
signature, timestamp = extract_slack_headers(headers_dict)
|
|
635
|
+
|
|
636
|
+
if not signature:
|
|
637
|
+
logger.error("Missing X-Slack-Signature header")
|
|
638
|
+
raise HTTPException(
|
|
639
|
+
status_code=401,
|
|
640
|
+
detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if not timestamp:
|
|
644
|
+
logger.error("Missing X-Slack-Request-Timestamp header")
|
|
645
|
+
raise HTTPException(
|
|
646
|
+
status_code=401,
|
|
647
|
+
detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
body = await request.body()
|
|
651
|
+
body_str = body.decode('utf-8')
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
verify_slack_signature(
|
|
655
|
+
signing_secret=signing_secret,
|
|
656
|
+
timestamp=timestamp,
|
|
657
|
+
body=body_str,
|
|
658
|
+
signature=signature
|
|
659
|
+
)
|
|
660
|
+
logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
|
|
661
|
+
except SlackSignatureVerificationError as e:
|
|
662
|
+
logger.error(f"Slack signature verification failed: {e}")
|
|
663
|
+
raise HTTPException(
|
|
664
|
+
status_code=401,
|
|
665
|
+
detail=f"Slack signature verification failed: {str(e)}"
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
# Store verification info in request state
|
|
669
|
+
request.state.slack_verified = True
|
|
670
|
+
request.state.slack_timestamp = timestamp
|
|
671
|
+
|
|
672
|
+
except HTTPException:
|
|
673
|
+
raise
|
|
674
|
+
except Exception as e:
|
|
675
|
+
logger.error(f"Unexpected error during Slack webhook authentication: {e}")
|
|
676
|
+
raise HTTPException(
|
|
677
|
+
status_code=500,
|
|
678
|
+
detail=f"Authentication error: {str(e)}"
|
|
679
|
+
)
|
|
680
|
+
|
|
595
681
|
def get_app(self) -> FastAPI:
|
|
596
682
|
"""Get the FastAPI app instance."""
|
|
597
683
|
return self.app
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Authentication utilities for trigger webhooks."""
|
|
2
|
+
|
|
3
|
+
from .slack_hmac import (
|
|
4
|
+
verify_slack_signature,
|
|
5
|
+
get_slack_signing_secret,
|
|
6
|
+
extract_slack_headers,
|
|
7
|
+
SlackSignatureVerificationError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"verify_slack_signature",
|
|
12
|
+
"get_slack_signing_secret",
|
|
13
|
+
"extract_slack_headers",
|
|
14
|
+
"SlackSignatureVerificationError",
|
|
15
|
+
]
|
|
16
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Slack HMAC signature verification for webhook authentication.
|
|
2
|
+
|
|
3
|
+
Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
|
|
4
|
+
Each request includes an X-Slack-Signature header that must be verified
|
|
5
|
+
against your app's signing secret.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import hmac
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SlackSignatureVerificationError(Exception):
|
|
21
|
+
"""Exception raised when Slack signature verification fails."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def verify_slack_signature(
|
|
26
|
+
signing_secret: str,
|
|
27
|
+
timestamp: str,
|
|
28
|
+
body: str,
|
|
29
|
+
signature: str,
|
|
30
|
+
max_age_seconds: int = 300
|
|
31
|
+
) -> bool:
|
|
32
|
+
try:
|
|
33
|
+
# Verify timestamp to prevent replay attacks
|
|
34
|
+
current_time = int(time.time())
|
|
35
|
+
request_time = int(timestamp)
|
|
36
|
+
|
|
37
|
+
if abs(current_time - request_time) > max_age_seconds:
|
|
38
|
+
logger.error(
|
|
39
|
+
f"Slack request timestamp too old. "
|
|
40
|
+
f"Current: {current_time}, Request: {request_time}, "
|
|
41
|
+
f"Diff: {abs(current_time - request_time)}s"
|
|
42
|
+
)
|
|
43
|
+
raise SlackSignatureVerificationError(
|
|
44
|
+
f"Request timestamp is too old (>{max_age_seconds}s). Possible replay attack."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Format: v0:{timestamp}:{body}
|
|
48
|
+
sig_basestring = f"v0:{timestamp}:{body}"
|
|
49
|
+
|
|
50
|
+
# Create HMAC-SHA256 hash
|
|
51
|
+
my_signature = 'v0=' + hmac.new(
|
|
52
|
+
signing_secret.encode(),
|
|
53
|
+
sig_basestring.encode(),
|
|
54
|
+
hashlib.sha256
|
|
55
|
+
).hexdigest()
|
|
56
|
+
|
|
57
|
+
if not hmac.compare_digest(my_signature, signature):
|
|
58
|
+
logger.error(
|
|
59
|
+
f"Slack signature mismatch. "
|
|
60
|
+
f"Expected: {my_signature}, Got: {signature}"
|
|
61
|
+
)
|
|
62
|
+
raise SlackSignatureVerificationError("Signature verification failed")
|
|
63
|
+
|
|
64
|
+
logger.info("Successfully verified Slack webhook signature")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
logger.error(f"Invalid timestamp format: {e}")
|
|
69
|
+
raise SlackSignatureVerificationError(f"Invalid timestamp: {str(e)}")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Unexpected error during Slack signature verification: {e}")
|
|
72
|
+
raise SlackSignatureVerificationError(f"Verification error: {str(e)}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_slack_signing_secret() -> Optional[str]:
|
|
76
|
+
"""Get Slack signing secret from SLACK_SIGNING_SECRET environment variable."""
|
|
77
|
+
secret = os.getenv("SLACK_SIGNING_SECRET")
|
|
78
|
+
if not secret:
|
|
79
|
+
logger.warning("SLACK_SIGNING_SECRET environment variable not set")
|
|
80
|
+
return secret
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_slack_headers(headers: dict) -> tuple[Optional[str], Optional[str]]:
|
|
84
|
+
"""Extract Slack signature and timestamp from request headers."""
|
|
85
|
+
signature = (
|
|
86
|
+
headers.get('x-slack-signature') or
|
|
87
|
+
headers.get('X-Slack-Signature')
|
|
88
|
+
)
|
|
89
|
+
timestamp = (
|
|
90
|
+
headers.get('x-slack-request-timestamp') or
|
|
91
|
+
headers.get('X-Slack-Request-Timestamp')
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return signature, timestamp
|
|
95
|
+
|
|
File without changes
|
{langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/.vscode/settings.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langchain_trigger_server-0.2.6rc1 → langchain_trigger_server-0.2.6rc3}/langchain_triggers/core.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|