langchain-trigger-server 0.2.6rc2__tar.gz → 0.2.6rc4__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.

Files changed (19) hide show
  1. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/PKG-INFO +1 -1
  2. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/app.py +162 -3
  3. langchain_trigger_server-0.2.6rc4/langchain_triggers/auth/__init__.py +16 -0
  4. langchain_trigger_server-0.2.6rc4/langchain_triggers/auth/slack_hmac.py +95 -0
  5. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/pyproject.toml +1 -1
  6. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/.github/workflows/release.yml +0 -0
  7. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/.vscode/settings.json +0 -0
  8. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/README.md +0 -0
  9. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/__init__.py +0 -0
  10. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/core.py +0 -0
  11. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/cron_manager.py +0 -0
  12. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/database/__init__.py +0 -0
  13. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/database/interface.py +0 -0
  14. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/database/supabase.py +0 -0
  15. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/decorators.py +0 -0
  16. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/triggers/__init__.py +0 -0
  17. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/langchain_triggers/triggers/cron_trigger.py +0 -0
  18. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/test_framework.py +0 -0
  19. {langchain_trigger_server-0.2.6rc2 → langchain_trigger_server-0.2.6rc4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-trigger-server
3
- Version: 0.2.6rc2
3
+ Version: 0.2.6rc4
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
@@ -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
 
@@ -486,11 +492,21 @@ class TriggerServer:
486
492
  ) -> Dict[str, Any]:
487
493
  """Handle an incoming request with a handler function."""
488
494
  try:
489
-
490
- # Parse request data
491
495
  if request.method == "POST":
492
496
  if request.headers.get("content-type", "").startswith("application/json"):
493
- payload = await request.json()
497
+ # Read body once for both auth and parsing
498
+ body_bytes = await request.body()
499
+ body_str = body_bytes.decode("utf-8")
500
+
501
+ if self._is_slack_trigger(trigger):
502
+ await self._verify_slack_webhook_auth_with_body(request, body_str)
503
+
504
+ import json
505
+ payload = json.loads(body_str)
506
+
507
+ if payload.get("type") == "url_verification" and "challenge" in payload:
508
+ logger.info(f"Responding to Slack URL verification challenge")
509
+ return {"challenge": payload["challenge"]}
494
510
  else:
495
511
  # Handle form data or other content types
496
512
  body = await request.body()
@@ -592,6 +608,149 @@ class TriggerServer:
592
608
  logger.error(f"Error invoking agent {agent_id}: {e}")
593
609
  raise
594
610
 
611
+ def _is_slack_trigger(self, trigger: TriggerTemplate) -> bool:
612
+ """Check if a trigger is from Slack and requires HMAC signature verification."""
613
+ return (
614
+ trigger.provider.lower() == "slack" or
615
+ "slack" in trigger.id.lower()
616
+ )
617
+
618
+ async def _verify_slack_webhook_auth(self, request: Request) -> None:
619
+ """Verify Slack HMAC signature for webhook requests.
620
+
621
+ Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
622
+ The signature is computed from the timestamp, body, and signing secret.
623
+
624
+ Args:
625
+ request: The FastAPI request object
626
+
627
+ Raises:
628
+ HTTPException: If authentication fails
629
+ """
630
+ try:
631
+ signing_secret = get_slack_signing_secret()
632
+ if not signing_secret:
633
+ logger.error("SLACK_SIGNING_SECRET environment variable not set")
634
+ raise HTTPException(
635
+ status_code=500,
636
+ detail="Slack signing secret not configured on server"
637
+ )
638
+
639
+ headers_dict = dict(request.headers)
640
+ signature, timestamp = extract_slack_headers(headers_dict)
641
+
642
+ if not signature:
643
+ logger.error("Missing X-Slack-Signature header")
644
+ raise HTTPException(
645
+ status_code=401,
646
+ detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
647
+ )
648
+
649
+ if not timestamp:
650
+ logger.error("Missing X-Slack-Request-Timestamp header")
651
+ raise HTTPException(
652
+ status_code=401,
653
+ detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
654
+ )
655
+
656
+ body = await request.body()
657
+ body_str = body.decode('utf-8')
658
+
659
+ try:
660
+ verify_slack_signature(
661
+ signing_secret=signing_secret,
662
+ timestamp=timestamp,
663
+ body=body_str,
664
+ signature=signature
665
+ )
666
+ logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
667
+ except SlackSignatureVerificationError as e:
668
+ logger.error(f"Slack signature verification failed: {e}")
669
+ raise HTTPException(
670
+ status_code=401,
671
+ detail=f"Slack signature verification failed: {str(e)}"
672
+ )
673
+
674
+ # Store verification info in request state
675
+ request.state.slack_verified = True
676
+ request.state.slack_timestamp = timestamp
677
+
678
+ except HTTPException:
679
+ raise
680
+ except Exception as e:
681
+ logger.error(f"Unexpected error during Slack webhook authentication: {e}")
682
+ raise HTTPException(
683
+ status_code=500,
684
+ detail=f"Authentication error: {str(e)}"
685
+ )
686
+
687
+ async def _verify_slack_webhook_auth_with_body(self, request: Request, body_str: str) -> None:
688
+ """Verify Slack HMAC signature for webhook requests using pre-read body.
689
+
690
+ Slack uses HMAC-SHA256 signatures to verify webhook authenticity.
691
+ The signature is computed from the timestamp, body, and signing secret.
692
+
693
+ Args:
694
+ request: The FastAPI request object
695
+ body_str: The request body as a string (already read)
696
+
697
+ Raises:
698
+ HTTPException: If authentication fails
699
+ """
700
+ try:
701
+ signing_secret = get_slack_signing_secret()
702
+ if not signing_secret:
703
+ logger.error("SLACK_SIGNING_SECRET environment variable not set")
704
+ raise HTTPException(
705
+ status_code=500,
706
+ detail="Slack signing secret not configured on server"
707
+ )
708
+
709
+ headers_dict = dict(request.headers)
710
+ signature, timestamp = extract_slack_headers(headers_dict)
711
+
712
+ if not signature:
713
+ logger.error("Missing X-Slack-Signature header")
714
+ raise HTTPException(
715
+ status_code=401,
716
+ detail="Missing X-Slack-Signature header. Slack webhooks require signature verification."
717
+ )
718
+
719
+ if not timestamp:
720
+ logger.error("Missing X-Slack-Request-Timestamp header")
721
+ raise HTTPException(
722
+ status_code=401,
723
+ detail="Missing X-Slack-Request-Timestamp header. Slack webhooks require timestamp."
724
+ )
725
+
726
+ try:
727
+ verify_slack_signature(
728
+ signing_secret=signing_secret,
729
+ timestamp=timestamp,
730
+ body=body_str,
731
+ signature=signature
732
+ )
733
+ logger.info(f"Successfully verified Slack webhook signature. Timestamp: {timestamp}")
734
+ except SlackSignatureVerificationError as e:
735
+ logger.error(f"Slack signature verification failed: {e}")
736
+ raise HTTPException(
737
+ status_code=401,
738
+ detail=f"Slack signature verification failed: {str(e)}"
739
+ )
740
+
741
+ # Store verification info in request state
742
+ request.state.slack_verified = True
743
+ request.state.slack_timestamp = timestamp
744
+
745
+ except HTTPException:
746
+ raise
747
+ except Exception as e:
748
+ logger.error(f"Unexpected error during Slack webhook authentication: {e}")
749
+ raise HTTPException(
750
+ status_code=500,
751
+ detail=f"Authentication error: {str(e)}"
752
+ )
753
+
595
754
  def get_app(self) -> FastAPI:
596
755
  """Get the FastAPI app instance."""
597
756
  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
+
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langchain-trigger-server"
7
- version = "0.2.6rc2"
7
+ version = "0.2.6rc4"
8
8
  description = "Generic event-driven triggers framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"