- gateway/normalize.py: normalize_slack_event -> KonstructMessage (strips bot mention) - gateway/channels/slack.py: register_slack_handlers for app_mention + DM events - rate limit check -> ephemeral rejection on exceeded - idempotency dedup (Slack retry protection) - placeholder 'Thinking...' message posted in-thread before Celery dispatch - auto-follow engaged threads with 30-minute TTL - HTTP 200 returned immediately; all LLM work dispatched to Celery - gateway/main.py: FastAPI on port 8001, /slack/events + /health - router/tenant.py: resolve_tenant workspace_id -> tenant_id (RLS-bypass query) - router/ratelimit.py: check_rate_limit Redis token bucket, RateLimitExceeded exception - router/idempotency.py: is_duplicate + mark_processed (SET NX, 24h TTL) - router/context.py: load_agent_for_tenant with RLS ContextVar setup - orchestrator/tasks.py: handle_message now extracts placeholder_ts/channel_id, calls _update_slack_placeholder via chat.update after LLM response - docker-compose.yml: gateway service on port 8001 - pyproject.toml: added redis, konstruct-router, konstruct-orchestrator deps
65 lines
1.9 KiB
Python
65 lines
1.9 KiB
Python
"""
|
|
Slack request signature verification.
|
|
|
|
slack-bolt's AsyncApp handles signature verification automatically when
|
|
initialized with a signing_secret. This module provides a standalone
|
|
helper for contexts that require manual verification (e.g., testing,
|
|
custom middleware layers).
|
|
|
|
In production, prefer slack-bolt's built-in verification — do NOT disable
|
|
it or bypass it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import time
|
|
|
|
|
|
def verify_slack_signature(
|
|
body: bytes,
|
|
timestamp: str,
|
|
signature: str,
|
|
signing_secret: str,
|
|
max_age_seconds: int = 300,
|
|
) -> bool:
|
|
"""
|
|
Verify a Slack webhook request signature.
|
|
|
|
Implements Slack's signing secret verification algorithm:
|
|
https://api.slack.com/authentication/verifying-requests-from-slack
|
|
|
|
Args:
|
|
body: Raw request body bytes.
|
|
timestamp: Value of the ``X-Slack-Request-Timestamp`` header.
|
|
signature: Value of the ``X-Slack-Signature`` header.
|
|
signing_secret: App's signing secret from Slack dashboard.
|
|
max_age_seconds: Reject requests older than this (replay protection).
|
|
|
|
Returns:
|
|
True if signature is valid and request is fresh, False otherwise.
|
|
"""
|
|
# Replay attack prevention — reject stale requests
|
|
try:
|
|
request_age = abs(int(time.time()) - int(timestamp))
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
if request_age > max_age_seconds:
|
|
return False
|
|
|
|
# Compute expected signature
|
|
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8', errors='replace')}"
|
|
computed = (
|
|
"v0="
|
|
+ hmac.new(
|
|
signing_secret.encode("utf-8"),
|
|
sig_basestring.encode("utf-8"),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
)
|
|
|
|
# Constant-time comparison to prevent timing attacks
|
|
return hmac.compare_digest(computed, signature)
|