langchain-trigger-server 0.1.5__tar.gz → 0.1.7__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.1.5 → langchain_trigger_server-0.1.7}/PKG-INFO +3 -1
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/app.py +57 -20
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/database/interface.py +8 -2
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/database/supabase.py +76 -4
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/pyproject.toml +3 -1
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/.github/workflows/release.yml +0 -0
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/README.md +0 -0
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/__init__.py +0 -0
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/core.py +0 -0
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/database/__init__.py +0 -0
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/decorators.py +0 -0
- {langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/test_framework.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-trigger-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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,12 +16,14 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: cryptography>=3.0.0
|
|
19
20
|
Requires-Dist: fastapi>=0.100.0
|
|
20
21
|
Requires-Dist: httpx>=0.24.0
|
|
21
22
|
Requires-Dist: langgraph-sdk>=0.2.6
|
|
22
23
|
Requires-Dist: pydantic>=2.0.0
|
|
23
24
|
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
24
25
|
Requires-Dist: python-multipart>=0.0.6
|
|
26
|
+
Requires-Dist: supabase>=2.0.0
|
|
25
27
|
Requires-Dist: uvicorn[standard]>=0.20.0
|
|
26
28
|
Provides-Extra: dev
|
|
27
29
|
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
@@ -242,6 +242,29 @@ class TriggerServer:
|
|
|
242
242
|
if not trigger:
|
|
243
243
|
raise HTTPException(status_code=400, detail=f"Unknown trigger type: {trigger_id}")
|
|
244
244
|
|
|
245
|
+
# Parse payload into registration model first
|
|
246
|
+
try:
|
|
247
|
+
registration_instance = trigger.registration_model(**payload)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
raise HTTPException(
|
|
250
|
+
status_code=400,
|
|
251
|
+
detail=f"Invalid payload for trigger: {str(e)}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Check for duplicate registration based on resource data
|
|
255
|
+
resource_dict = registration_instance.model_dump()
|
|
256
|
+
existing_registration = await self.database.find_registration_by_resource(
|
|
257
|
+
template_id=trigger.id,
|
|
258
|
+
resource_data=resource_dict,
|
|
259
|
+
user_id=user_id
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if existing_registration:
|
|
263
|
+
raise HTTPException(
|
|
264
|
+
status_code=400,
|
|
265
|
+
detail=f"A registration with this configuration already exists for trigger type '{trigger.id}'. Registration ID: {existing_registration.get('id')}"
|
|
266
|
+
)
|
|
267
|
+
|
|
245
268
|
# Inject OAuth tokens if needed for registration
|
|
246
269
|
auth_user = None
|
|
247
270
|
if trigger.oauth_providers:
|
|
@@ -267,19 +290,10 @@ class TriggerServer:
|
|
|
267
290
|
raise HTTPException(status_code=500, detail="OAuth authentication failed")
|
|
268
291
|
|
|
269
292
|
|
|
270
|
-
# Parse payload into registration model first
|
|
271
|
-
try:
|
|
272
|
-
registration_instance = trigger.registration_model(**payload)
|
|
273
|
-
except Exception as e:
|
|
274
|
-
raise HTTPException(
|
|
275
|
-
status_code=400,
|
|
276
|
-
detail=f"Invalid payload for trigger: {str(e)}"
|
|
277
|
-
)
|
|
278
|
-
|
|
279
293
|
# Call the trigger's registration handler with parsed registration model
|
|
280
294
|
result = await trigger.registration_handler(registration_instance, auth_user)
|
|
281
|
-
|
|
282
295
|
resource_dict = registration_instance.model_dump()
|
|
296
|
+
|
|
283
297
|
registration = await self.database.create_trigger_registration(
|
|
284
298
|
user_id=user_id,
|
|
285
299
|
template_id=trigger.id,
|
|
@@ -423,6 +437,24 @@ class TriggerServer:
|
|
|
423
437
|
) -> Dict[str, Any]:
|
|
424
438
|
"""Handle an incoming request with a handler function."""
|
|
425
439
|
try:
|
|
440
|
+
# Step 1: API Key Authentication (required for webhooks)
|
|
441
|
+
api_key = request.headers.get("x-api-key")
|
|
442
|
+
if not api_key:
|
|
443
|
+
logger.warning("Webhook request missing x-api-key header")
|
|
444
|
+
raise HTTPException(
|
|
445
|
+
status_code=401,
|
|
446
|
+
detail="Missing x-api-key header"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Validate API key and get user_id
|
|
450
|
+
user_id = await self.database.validate_api_key(api_key)
|
|
451
|
+
if not user_id:
|
|
452
|
+
logger.warning("Invalid API key provided to webhook")
|
|
453
|
+
raise HTTPException(
|
|
454
|
+
status_code=401,
|
|
455
|
+
detail="Invalid API key"
|
|
456
|
+
)
|
|
457
|
+
|
|
426
458
|
# Parse request data
|
|
427
459
|
if request.method == "POST":
|
|
428
460
|
if request.headers.get("content-type", "").startswith("application/json"):
|
|
@@ -434,7 +466,7 @@ class TriggerServer:
|
|
|
434
466
|
else:
|
|
435
467
|
payload = dict(request.query_params)
|
|
436
468
|
|
|
437
|
-
# Step
|
|
469
|
+
# Step 2: Registration resolution
|
|
438
470
|
if not trigger.registration_resolver:
|
|
439
471
|
raise HTTPException(
|
|
440
472
|
status_code=500,
|
|
@@ -444,24 +476,22 @@ class TriggerServer:
|
|
|
444
476
|
# Extract resource identifiers from webhook payload
|
|
445
477
|
resource_data = await trigger.registration_resolver(payload)
|
|
446
478
|
|
|
447
|
-
# Find matching registration
|
|
479
|
+
# Find matching registration for the authenticated user
|
|
448
480
|
# Convert Pydantic model to dict for database lookup
|
|
449
481
|
resource_dict = resource_data.model_dump()
|
|
450
482
|
registration = await self.database.find_registration_by_resource(
|
|
451
483
|
trigger.id,
|
|
452
|
-
resource_dict
|
|
484
|
+
resource_dict,
|
|
485
|
+
user_id
|
|
453
486
|
)
|
|
454
487
|
|
|
455
488
|
if not registration:
|
|
456
|
-
logger.warning(f"No registration found for trigger_id={trigger.id} with resource={resource_data}
|
|
489
|
+
logger.warning(f"No registration found for user {user_id}, trigger_id={trigger.id} with resource={resource_data}")
|
|
457
490
|
raise HTTPException(
|
|
458
491
|
status_code=400,
|
|
459
492
|
detail=f"No registration found for {trigger.id} with resource {resource_data}"
|
|
460
493
|
)
|
|
461
494
|
|
|
462
|
-
# Step 2: Get user_id from registration (webhooks don't use API auth)
|
|
463
|
-
user_id = registration["user_id"]
|
|
464
|
-
|
|
465
495
|
# Step 3: Inject OAuth tokens if needed
|
|
466
496
|
auth_user = None
|
|
467
497
|
if trigger.oauth_providers and self.langchain_auth_client:
|
|
@@ -571,9 +601,17 @@ class TriggerServer:
|
|
|
571
601
|
agent_id=agent_id
|
|
572
602
|
)
|
|
573
603
|
|
|
574
|
-
|
|
604
|
+
thread = await self.langgraph_client.threads.create(
|
|
605
|
+
metadata={
|
|
606
|
+
"triggered_by": "langchain-triggers",
|
|
607
|
+
"user_id": user_id,
|
|
608
|
+
},
|
|
609
|
+
headers=headers,
|
|
610
|
+
)
|
|
611
|
+
logger.info(f"Created thread {thread['thread_id']} for agent {agent_id}")
|
|
612
|
+
|
|
575
613
|
run = await self.langgraph_client.runs.create(
|
|
576
|
-
thread_id=
|
|
614
|
+
thread_id=thread['thread_id'],
|
|
577
615
|
assistant_id=agent_id,
|
|
578
616
|
input=input_data,
|
|
579
617
|
metadata={
|
|
@@ -581,7 +619,6 @@ class TriggerServer:
|
|
|
581
619
|
"user_id": user_id,
|
|
582
620
|
},
|
|
583
621
|
headers=headers,
|
|
584
|
-
if_not_exists="create",
|
|
585
622
|
)
|
|
586
623
|
|
|
587
624
|
logger.info(f"Successfully invoked agent {agent_id}, run_id: {run['run_id']}, thread_id: {run['thread_id']}")
|
|
@@ -61,9 +61,10 @@ class TriggerDatabaseInterface(ABC):
|
|
|
61
61
|
async def find_registration_by_resource(
|
|
62
62
|
self,
|
|
63
63
|
template_id: str,
|
|
64
|
-
resource_data: Dict[str, Any]
|
|
64
|
+
resource_data: Dict[str, Any],
|
|
65
|
+
user_id: str
|
|
65
66
|
) -> Optional[Dict[str, Any]]:
|
|
66
|
-
"""Find trigger registration by matching resource data."""
|
|
67
|
+
"""Find trigger registration by matching resource data for a specific user."""
|
|
67
68
|
pass
|
|
68
69
|
|
|
69
70
|
@abstractmethod
|
|
@@ -127,4 +128,9 @@ class TriggerDatabaseInterface(ABC):
|
|
|
127
128
|
@abstractmethod
|
|
128
129
|
async def get_user_by_email(self, email: str) -> Optional[str]:
|
|
129
130
|
"""Get user ID by email from trigger registrations."""
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def validate_api_key(self, api_key: str) -> Optional[str]:
|
|
135
|
+
"""Validate API key and return user_id if valid, None if invalid."""
|
|
130
136
|
pass
|
|
@@ -4,6 +4,10 @@ import os
|
|
|
4
4
|
import logging
|
|
5
5
|
from typing import List, Optional, Dict, Any
|
|
6
6
|
from supabase import create_client, Client
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
10
|
+
from cryptography.hazmat.backends import default_backend
|
|
7
11
|
|
|
8
12
|
from .interface import TriggerDatabaseInterface
|
|
9
13
|
|
|
@@ -21,8 +25,47 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
21
25
|
raise ValueError("SUPABASE_URL and SUPABASE_KEY environment variables are required")
|
|
22
26
|
|
|
23
27
|
self.client = create_client(self.supabase_url, self.supabase_key)
|
|
28
|
+
|
|
29
|
+
# Get encryption key for API key decryption - required
|
|
30
|
+
self.encryption_key = os.getenv("SECRETS_ENCRYPTION_KEY")
|
|
31
|
+
if not self.encryption_key:
|
|
32
|
+
raise ValueError("SECRETS_ENCRYPTION_KEY environment variable is required")
|
|
33
|
+
|
|
24
34
|
logger.info("Initialized SupabaseTriggerDatabase")
|
|
25
35
|
|
|
36
|
+
def _decrypt_secret(self, encrypted_secret: str) -> str:
|
|
37
|
+
"""Decrypt an encrypted secret using AES-256-GCM to match OAP Node.js implementation."""
|
|
38
|
+
try:
|
|
39
|
+
# Decode the base64 encoded encrypted data
|
|
40
|
+
combined = base64.b64decode(encrypted_secret)
|
|
41
|
+
|
|
42
|
+
# Constants from Node.js implementation
|
|
43
|
+
IV_LENGTH = 12 # 96 bits
|
|
44
|
+
TAG_LENGTH = 16 # 128 bits
|
|
45
|
+
|
|
46
|
+
# Minimum length check
|
|
47
|
+
if len(combined) < IV_LENGTH + TAG_LENGTH + 1:
|
|
48
|
+
raise ValueError("Invalid encrypted secret format: too short or malformed")
|
|
49
|
+
|
|
50
|
+
# Extract IV, encrypted data, and auth tag
|
|
51
|
+
iv = combined[:IV_LENGTH]
|
|
52
|
+
tag = combined[-TAG_LENGTH:]
|
|
53
|
+
encrypted_data = combined[IV_LENGTH:-TAG_LENGTH]
|
|
54
|
+
|
|
55
|
+
# Derive key using SHA-256 hash (same as Node.js deriveKey function)
|
|
56
|
+
key = hashlib.sha256(self.encryption_key.encode()).digest()
|
|
57
|
+
|
|
58
|
+
# Create AES-GCM cipher
|
|
59
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend())
|
|
60
|
+
decryptor = cipher.decryptor()
|
|
61
|
+
|
|
62
|
+
# Decrypt the data
|
|
63
|
+
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
64
|
+
|
|
65
|
+
return decrypted_data.decode('utf-8')
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Error decrypting secret: {e}")
|
|
68
|
+
raise ValueError("Failed to decrypt API key")
|
|
26
69
|
|
|
27
70
|
# ========== Trigger Templates ==========
|
|
28
71
|
|
|
@@ -182,14 +225,15 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
182
225
|
async def find_registration_by_resource(
|
|
183
226
|
self,
|
|
184
227
|
template_id: str,
|
|
185
|
-
resource_data: Dict[str, Any]
|
|
228
|
+
resource_data: Dict[str, Any],
|
|
229
|
+
user_id: str
|
|
186
230
|
) -> Optional[Dict[str, Any]]:
|
|
187
|
-
"""Find trigger registration by matching resource data."""
|
|
231
|
+
"""Find trigger registration by matching resource data for a specific user."""
|
|
188
232
|
try:
|
|
189
|
-
# Build query to match against trigger_registrations with template_id
|
|
233
|
+
# Build query to match against trigger_registrations with template_id and user_id filters
|
|
190
234
|
query = self.client.table("trigger_registrations").select(
|
|
191
235
|
"*, trigger_templates(id, name, description)"
|
|
192
|
-
).eq("trigger_templates.id", template_id)
|
|
236
|
+
).eq("trigger_templates.id", template_id).eq("user_id", user_id)
|
|
193
237
|
|
|
194
238
|
# Add resource field matches
|
|
195
239
|
for field, value in resource_data.items():
|
|
@@ -300,4 +344,32 @@ class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
|
300
344
|
|
|
301
345
|
except Exception as e:
|
|
302
346
|
logger.error(f"Error getting user by email: {e}")
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
async def validate_api_key(self, api_key: str) -> Optional[str]:
|
|
350
|
+
"""Validate API key and return user_id if valid, None if invalid."""
|
|
351
|
+
try:
|
|
352
|
+
# Query all user API keys to find a match
|
|
353
|
+
response = self.client.table("user_api_keys").select("user_id, key_hash").execute()
|
|
354
|
+
|
|
355
|
+
if not response.data:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
# Check each encrypted key to see if it matches the provided key
|
|
359
|
+
for row in response.data:
|
|
360
|
+
try:
|
|
361
|
+
decrypted_key = self._decrypt_secret(row["key_hash"])
|
|
362
|
+
if decrypted_key == api_key:
|
|
363
|
+
logger.info(f"Valid API key authenticated for user: {row['user_id']}")
|
|
364
|
+
return row["user_id"]
|
|
365
|
+
except Exception as e:
|
|
366
|
+
# Skip keys that fail to decrypt
|
|
367
|
+
logger.debug(f"Failed to decrypt API key: {e}")
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
logger.warning(f"Invalid API key provided")
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"Error validating API key: {e}")
|
|
303
375
|
return None
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langchain-trigger-server"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
description = "Generic event-driven triggers framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -31,6 +31,8 @@ dependencies = [
|
|
|
31
31
|
"python-multipart>=0.0.6",
|
|
32
32
|
"python-jose[cryptography]>=3.3.0",
|
|
33
33
|
"langgraph-sdk>=0.2.6",
|
|
34
|
+
"supabase>=2.0.0",
|
|
35
|
+
"cryptography>=3.0.0",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.optional-dependencies]
|
{langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
{langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/__init__.py
RENAMED
|
File without changes
|
{langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/core.py
RENAMED
|
File without changes
|
|
File without changes
|
{langchain_trigger_server-0.1.5 → langchain_trigger_server-0.1.7}/langchain_triggers/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|