langchain-trigger-server 0.3.4__tar.gz → 0.3.6__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.3.4 → langchain_trigger_server-0.3.6}/PKG-INFO +1 -1
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/app.py +31 -39
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/core.py +0 -6
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/cron_manager.py +3 -5
- langchain_trigger_server-0.3.6/langchain_triggers/database/__init__.py +5 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/database/interface.py +18 -33
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/triggers/cron_trigger.py +1 -6
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/pyproject.toml +1 -1
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/tests/unit/test_trigger_server_api.py +85 -35
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/uv.lock +1 -55
- langchain_trigger_server-0.3.6/version_comparison.txt +1 -0
- langchain_trigger_server-0.3.4/langchain_triggers/database/__init__.py +0 -16
- langchain_trigger_server-0.3.4/langchain_triggers/database/supabase.py +0 -471
- langchain_trigger_server-0.3.4/version_comparison.txt +0 -1
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/actions/uv_setup/action.yml +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/_lint.yml +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/_test.yml +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/ci.yml +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/release.yml +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.gitignore +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/Makefile +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/README.md +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/__init__.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/auth/__init__.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/auth/slack_hmac.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/decorators.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/triggers/__init__.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/tests/__init__.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/tests/unit/__init__.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/tests/unit/test_cron_manager_polling_filter.py +0 -0
- {langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/tests/unit/test_cron_manager_schedule_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-trigger-server
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -23,7 +23,7 @@ from .auth.slack_hmac import (
|
|
|
23
23
|
)
|
|
24
24
|
from .core import TriggerType
|
|
25
25
|
from .cron_manager import CronTriggerManager
|
|
26
|
-
from .database import TriggerDatabaseInterface
|
|
26
|
+
from .database import TriggerDatabaseInterface
|
|
27
27
|
from .decorators import TriggerTemplate
|
|
28
28
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
@@ -68,17 +68,20 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
68
68
|
return await call_next(request)
|
|
69
69
|
|
|
70
70
|
try:
|
|
71
|
-
# Run mandatory custom authentication
|
|
72
71
|
identity = await self.auth_handler({}, dict(request.headers))
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
if (
|
|
73
|
+
not identity
|
|
74
|
+
or not identity.get("identity")
|
|
75
|
+
or not identity.get("tenant_id")
|
|
76
|
+
):
|
|
77
|
+
logger.error(
|
|
78
|
+
f"Authentication failed: missing required fields (identity={bool(identity.get('identity') if identity else None)}, tenant_id={bool(identity.get('tenant_id') if identity else None)})"
|
|
79
|
+
)
|
|
75
80
|
return Response(
|
|
76
|
-
content='{"detail": "Authentication required"}',
|
|
81
|
+
content='{"detail": "Authentication required - identity and tenant_id must be provided"}',
|
|
77
82
|
status_code=401,
|
|
78
83
|
media_type="application/json",
|
|
79
84
|
)
|
|
80
|
-
|
|
81
|
-
# Store identity in request state for endpoints to access
|
|
82
85
|
request.state.current_user = identity
|
|
83
86
|
|
|
84
87
|
except Exception as e:
|
|
@@ -105,9 +108,7 @@ class TriggerServer:
|
|
|
105
108
|
def __init__(
|
|
106
109
|
self,
|
|
107
110
|
auth_handler: Callable,
|
|
108
|
-
database: TriggerDatabaseInterface
|
|
109
|
-
database_type: str | None = "supabase",
|
|
110
|
-
**database_kwargs: Any,
|
|
111
|
+
database: TriggerDatabaseInterface,
|
|
111
112
|
):
|
|
112
113
|
# Configure uvicorn logging to use consistent formatting
|
|
113
114
|
self._configure_uvicorn_logging()
|
|
@@ -117,15 +118,7 @@ class TriggerServer:
|
|
|
117
118
|
description="Event-driven triggers framework",
|
|
118
119
|
version="0.1.0",
|
|
119
120
|
)
|
|
120
|
-
|
|
121
|
-
# Configure database: allow either instance injection or factory creation
|
|
122
|
-
# Defaults to Supabase for backward compatibility
|
|
123
|
-
if database and database_type != "supabase":
|
|
124
|
-
raise ValueError("Provide either 'database' or 'database_type', not both")
|
|
125
|
-
if database is not None:
|
|
126
|
-
self.database = database
|
|
127
|
-
else:
|
|
128
|
-
self.database = create_database(database_type, **database_kwargs)
|
|
121
|
+
self.database = database
|
|
129
122
|
self.auth_handler = auth_handler
|
|
130
123
|
|
|
131
124
|
# LangGraph configuration
|
|
@@ -286,14 +279,15 @@ class TriggerServer:
|
|
|
286
279
|
async def api_list_registrations(
|
|
287
280
|
current_user: dict[str, Any] = Depends(get_current_user),
|
|
288
281
|
) -> dict[str, Any]:
|
|
289
|
-
"""List user's trigger registrations (user-scoped)."""
|
|
282
|
+
"""List user's trigger registrations (user and tenant-scoped)."""
|
|
290
283
|
try:
|
|
291
284
|
user_id = current_user["identity"]
|
|
285
|
+
tenant_id = current_user["tenant_id"]
|
|
292
286
|
|
|
293
287
|
# Get user's trigger registrations with linked agents in a single query
|
|
294
288
|
user_registrations = (
|
|
295
289
|
await self.database.get_user_trigger_registrations_with_agents(
|
|
296
|
-
user_id
|
|
290
|
+
user_id, tenant_id
|
|
297
291
|
)
|
|
298
292
|
)
|
|
299
293
|
|
|
@@ -329,6 +323,7 @@ class TriggerServer:
|
|
|
329
323
|
logger.info(f"Registration payload received: {payload}")
|
|
330
324
|
|
|
331
325
|
user_id = current_user["identity"]
|
|
326
|
+
tenant_id = current_user["tenant_id"]
|
|
332
327
|
trigger_id = payload.get("type")
|
|
333
328
|
if not trigger_id:
|
|
334
329
|
raise HTTPException(
|
|
@@ -340,7 +335,6 @@ class TriggerServer:
|
|
|
340
335
|
raise HTTPException(
|
|
341
336
|
status_code=400, detail=f"Unknown trigger type: {trigger_id}"
|
|
342
337
|
)
|
|
343
|
-
client_metadata = payload.pop("metadata", None)
|
|
344
338
|
|
|
345
339
|
# Parse payload into registration model first
|
|
346
340
|
try:
|
|
@@ -350,11 +344,12 @@ class TriggerServer:
|
|
|
350
344
|
status_code=400, detail=f"Invalid payload for trigger: {str(e)}"
|
|
351
345
|
)
|
|
352
346
|
|
|
353
|
-
# Check for duplicate registration based on resource data within this user's scope
|
|
347
|
+
# Check for duplicate registration based on resource data within this user's tenant scope
|
|
354
348
|
resource_dict = registration_instance.model_dump()
|
|
355
349
|
existing_registration = (
|
|
356
350
|
await self.database.find_user_registration_by_resource(
|
|
357
351
|
user_id=user_id,
|
|
352
|
+
tenant_id=tenant_id,
|
|
358
353
|
template_id=trigger.id,
|
|
359
354
|
resource_data=resource_dict,
|
|
360
355
|
)
|
|
@@ -386,16 +381,12 @@ class TriggerServer:
|
|
|
386
381
|
|
|
387
382
|
resource_dict = registration_instance.model_dump()
|
|
388
383
|
|
|
389
|
-
merged_metadata = {}
|
|
390
|
-
if client_metadata:
|
|
391
|
-
merged_metadata["client_metadata"] = client_metadata
|
|
392
|
-
merged_metadata.update(result.metadata)
|
|
393
|
-
|
|
394
384
|
registration = await self.database.create_trigger_registration(
|
|
395
385
|
user_id=user_id,
|
|
386
|
+
tenant_id=tenant_id,
|
|
396
387
|
template_id=trigger.id,
|
|
397
388
|
resource=resource_dict,
|
|
398
|
-
metadata=
|
|
389
|
+
metadata=result.metadata,
|
|
399
390
|
)
|
|
400
391
|
|
|
401
392
|
if not registration:
|
|
@@ -427,8 +418,9 @@ class TriggerServer:
|
|
|
427
418
|
"""Delete a trigger registration."""
|
|
428
419
|
try:
|
|
429
420
|
user_id = current_user["identity"]
|
|
421
|
+
tenant_id = current_user["tenant_id"]
|
|
430
422
|
success = await self.database.delete_trigger_registration(
|
|
431
|
-
registration_id, user_id
|
|
423
|
+
registration_id, user_id, tenant_id
|
|
432
424
|
)
|
|
433
425
|
if not success:
|
|
434
426
|
raise HTTPException(
|
|
@@ -450,10 +442,11 @@ class TriggerServer:
|
|
|
450
442
|
"""List agents linked to this registration."""
|
|
451
443
|
try:
|
|
452
444
|
user_id = current_user["identity"]
|
|
445
|
+
tenant_id = current_user["tenant_id"]
|
|
453
446
|
|
|
454
447
|
# Get the specific trigger registration
|
|
455
448
|
trigger = await self.database.get_trigger_registration(
|
|
456
|
-
registration_id, user_id
|
|
449
|
+
registration_id, user_id, tenant_id
|
|
457
450
|
)
|
|
458
451
|
if not trigger:
|
|
459
452
|
raise HTTPException(
|
|
@@ -490,10 +483,11 @@ class TriggerServer:
|
|
|
490
483
|
field_selection = None
|
|
491
484
|
|
|
492
485
|
user_id = current_user["identity"]
|
|
486
|
+
tenant_id = current_user["tenant_id"]
|
|
493
487
|
|
|
494
488
|
# Verify the trigger registration exists and belongs to the user
|
|
495
489
|
registration = await self.database.get_trigger_registration(
|
|
496
|
-
registration_id, user_id
|
|
490
|
+
registration_id, user_id, tenant_id
|
|
497
491
|
)
|
|
498
492
|
if not registration:
|
|
499
493
|
raise HTTPException(
|
|
@@ -537,10 +531,11 @@ class TriggerServer:
|
|
|
537
531
|
"""Remove an agent from a trigger registration."""
|
|
538
532
|
try:
|
|
539
533
|
user_id = current_user["identity"]
|
|
534
|
+
tenant_id = current_user["tenant_id"]
|
|
540
535
|
|
|
541
536
|
# Verify the trigger registration exists and belongs to the user
|
|
542
537
|
registration = await self.database.get_trigger_registration(
|
|
543
|
-
registration_id, user_id
|
|
538
|
+
registration_id, user_id, tenant_id
|
|
544
539
|
)
|
|
545
540
|
if not registration:
|
|
546
541
|
raise HTTPException(
|
|
@@ -578,10 +573,11 @@ class TriggerServer:
|
|
|
578
573
|
"""Manually execute a cron trigger registration immediately."""
|
|
579
574
|
try:
|
|
580
575
|
user_id = current_user["identity"]
|
|
576
|
+
tenant_id = current_user["tenant_id"]
|
|
581
577
|
|
|
582
578
|
# Verify the trigger registration exists and belongs to the user
|
|
583
579
|
registration = await self.database.get_trigger_registration(
|
|
584
|
-
registration_id, user_id
|
|
580
|
+
registration_id, user_id, tenant_id
|
|
585
581
|
)
|
|
586
582
|
if not registration:
|
|
587
583
|
raise HTTPException(
|
|
@@ -751,11 +747,7 @@ class TriggerServer:
|
|
|
751
747
|
# Ensure agent_id and user_id are strings for JSON serialization
|
|
752
748
|
agent_id_str = str(agent_id)
|
|
753
749
|
user_id_str = str(result.registration["user_id"])
|
|
754
|
-
tenant_id_str = str(
|
|
755
|
-
result.registration.get("metadata", {})
|
|
756
|
-
.get("client_metadata", {})
|
|
757
|
-
.get("tenant_id")
|
|
758
|
-
)
|
|
750
|
+
tenant_id_str = str(result.registration["tenant_id"])
|
|
759
751
|
|
|
760
752
|
agent_input = {"messages": [{"role": "human", "content": message}]}
|
|
761
753
|
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/core.py
RENAMED
|
@@ -97,12 +97,6 @@ class TriggerRegistrationResult(BaseModel):
|
|
|
97
97
|
if self.create_registration and not self.metadata:
|
|
98
98
|
self.metadata = {} # Allow empty metadata for create_registration=True
|
|
99
99
|
|
|
100
|
-
if "client_metadata" in self.metadata:
|
|
101
|
-
raise ValueError(
|
|
102
|
-
"The 'client_metadata' key is reserved for client-provided metadata. "
|
|
103
|
-
"Registration handlers must not use this key in their metadata."
|
|
104
|
-
)
|
|
105
|
-
|
|
106
100
|
if not self.create_registration and (
|
|
107
101
|
not self.response_body or not self.status_code
|
|
108
102
|
):
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/cron_manager.py
RENAMED
|
@@ -285,9 +285,7 @@ class CronTriggerManager:
|
|
|
285
285
|
user_id = registration["user_id"]
|
|
286
286
|
template_id = registration.get("template_id")
|
|
287
287
|
template_id = str(template_id) if template_id is not None else None
|
|
288
|
-
tenant_id = (
|
|
289
|
-
registration.get("metadata", {}).get("client_metadata", {}).get("tenant_id")
|
|
290
|
-
)
|
|
288
|
+
tenant_id = str(registration.get("tenant_id"))
|
|
291
289
|
|
|
292
290
|
# Get agent links
|
|
293
291
|
agent_links = await self.trigger_server.database.get_agents_for_trigger(
|
|
@@ -338,7 +336,7 @@ class CronTriggerManager:
|
|
|
338
336
|
f"agents_linked={len(agent_links)}"
|
|
339
337
|
)
|
|
340
338
|
|
|
341
|
-
for
|
|
339
|
+
for _message in messages:
|
|
342
340
|
for agent_link in agent_links:
|
|
343
341
|
agent_id = (
|
|
344
342
|
agent_link
|
|
@@ -357,7 +355,7 @@ class CronTriggerManager:
|
|
|
357
355
|
"messages": [
|
|
358
356
|
{
|
|
359
357
|
"role": "human",
|
|
360
|
-
"content": f"
|
|
358
|
+
"content": f"Cron trigger fired at {current_time_str}",
|
|
361
359
|
}
|
|
362
360
|
]
|
|
363
361
|
}
|
|
@@ -35,28 +35,33 @@ class TriggerDatabaseInterface(ABC):
|
|
|
35
35
|
|
|
36
36
|
@abstractmethod
|
|
37
37
|
async def create_trigger_registration(
|
|
38
|
-
self,
|
|
38
|
+
self,
|
|
39
|
+
user_id: str,
|
|
40
|
+
tenant_id: str,
|
|
41
|
+
template_id: str,
|
|
42
|
+
resource: dict,
|
|
43
|
+
metadata: dict = None,
|
|
39
44
|
) -> dict[str, Any] | None:
|
|
40
45
|
"""Create a new trigger registration for a user."""
|
|
41
46
|
pass
|
|
42
47
|
|
|
43
48
|
@abstractmethod
|
|
44
49
|
async def get_user_trigger_registrations(
|
|
45
|
-
self, user_id: str
|
|
50
|
+
self, user_id: str, tenant_id: str
|
|
46
51
|
) -> list[dict[str, Any]]:
|
|
47
|
-
"""Get all trigger registrations for a user."""
|
|
52
|
+
"""Get all trigger registrations for a user within a tenant."""
|
|
48
53
|
pass
|
|
49
54
|
|
|
50
55
|
@abstractmethod
|
|
51
56
|
async def get_user_trigger_registrations_with_agents(
|
|
52
|
-
self, user_id: str
|
|
57
|
+
self, user_id: str, tenant_id: str
|
|
53
58
|
) -> list[dict[str, Any]]:
|
|
54
|
-
"""Get all trigger registrations for a user with linked agents in a single query."""
|
|
59
|
+
"""Get all trigger registrations for a user with linked agents in a single query within a tenant."""
|
|
55
60
|
pass
|
|
56
61
|
|
|
57
62
|
@abstractmethod
|
|
58
63
|
async def get_trigger_registration(
|
|
59
|
-
self, registration_id: str, user_id: str
|
|
64
|
+
self, registration_id: str, user_id: str, tenant_id: str
|
|
60
65
|
) -> dict[str, Any] | None:
|
|
61
66
|
"""Get a specific trigger registration."""
|
|
62
67
|
pass
|
|
@@ -70,9 +75,13 @@ class TriggerDatabaseInterface(ABC):
|
|
|
70
75
|
|
|
71
76
|
@abstractmethod
|
|
72
77
|
async def find_user_registration_by_resource(
|
|
73
|
-
self,
|
|
78
|
+
self,
|
|
79
|
+
user_id: str,
|
|
80
|
+
tenant_id: str,
|
|
81
|
+
template_id: str,
|
|
82
|
+
resource_data: dict[str, Any],
|
|
74
83
|
) -> dict[str, Any] | None:
|
|
75
|
-
"""Find trigger registration by matching resource data for a specific user."""
|
|
84
|
+
"""Find trigger registration by matching resource data for a specific user within a tenant."""
|
|
76
85
|
pass
|
|
77
86
|
|
|
78
87
|
@abstractmethod
|
|
@@ -80,16 +89,9 @@ class TriggerDatabaseInterface(ABC):
|
|
|
80
89
|
"""Get all registrations for a specific trigger template."""
|
|
81
90
|
pass
|
|
82
91
|
|
|
83
|
-
@abstractmethod
|
|
84
|
-
async def update_trigger_metadata(
|
|
85
|
-
self, registration_id: str, metadata_updates: dict, user_id: str = None
|
|
86
|
-
) -> bool:
|
|
87
|
-
"""Update metadata for a trigger registration."""
|
|
88
|
-
pass
|
|
89
|
-
|
|
90
92
|
@abstractmethod
|
|
91
93
|
async def delete_trigger_registration(
|
|
92
|
-
self, registration_id: str, user_id: str
|
|
94
|
+
self, registration_id: str, user_id: str, tenant_id: str
|
|
93
95
|
) -> bool:
|
|
94
96
|
"""Delete a trigger registration."""
|
|
95
97
|
pass
|
|
@@ -120,20 +122,3 @@ class TriggerDatabaseInterface(ABC):
|
|
|
120
122
|
) -> list[dict[str, Any]]:
|
|
121
123
|
"""Get all agent links for a trigger registration with field_selection."""
|
|
122
124
|
pass
|
|
123
|
-
|
|
124
|
-
@abstractmethod
|
|
125
|
-
async def get_triggers_for_agent(self, agent_id: str) -> list[dict[str, Any]]:
|
|
126
|
-
"""Get all trigger registrations linked to an agent."""
|
|
127
|
-
pass
|
|
128
|
-
|
|
129
|
-
# ========== Helper Methods ==========
|
|
130
|
-
|
|
131
|
-
@abstractmethod
|
|
132
|
-
async def get_user_from_token(self, token: str) -> str | None:
|
|
133
|
-
"""Extract user ID from authentication token."""
|
|
134
|
-
pass
|
|
135
|
-
|
|
136
|
-
@abstractmethod
|
|
137
|
-
async def get_user_by_email(self, email: str) -> str | None:
|
|
138
|
-
"""Get user ID by email from trigger registrations."""
|
|
139
|
-
pass
|
|
@@ -84,12 +84,7 @@ async def cron_poll_handler(
|
|
|
84
84
|
|
|
85
85
|
Produces a simple time-based message for linked agents.
|
|
86
86
|
"""
|
|
87
|
-
|
|
88
|
-
current_time_str = current_time.strftime("%A, %B %d, %Y at %H:%M UTC")
|
|
89
|
-
message = (
|
|
90
|
-
"ACTION: triggering cron from langchain-trigger-server\n"
|
|
91
|
-
f"CURRENT TIME: {current_time_str}"
|
|
92
|
-
)
|
|
87
|
+
message = "Cron trigger fired"
|
|
93
88
|
return TriggerHandlerResult(
|
|
94
89
|
invoke_agent=True,
|
|
95
90
|
agent_messages=[message],
|
|
@@ -18,7 +18,10 @@ async def mock_auth_handler(request_body, headers):
|
|
|
18
18
|
return None
|
|
19
19
|
# Extract user ID from Bearer token for testing
|
|
20
20
|
token = auth_header.replace("Bearer ", "")
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
"identity": f"test_user_{token}",
|
|
23
|
+
"tenant_id": f"test_tenant_{token}",
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class TestRegistration(BaseModel):
|
|
@@ -61,11 +64,12 @@ class MockDatabase:
|
|
|
61
64
|
return self.templates
|
|
62
65
|
|
|
63
66
|
async def create_trigger_registration(
|
|
64
|
-
self, user_id, template_id, resource, metadata
|
|
67
|
+
self, user_id, tenant_id, template_id, resource, metadata
|
|
65
68
|
):
|
|
66
69
|
registration = {
|
|
67
70
|
"id": f"reg_{len(self.registrations)}",
|
|
68
71
|
"user_id": user_id,
|
|
72
|
+
"tenant_id": tenant_id,
|
|
69
73
|
"template_id": template_id,
|
|
70
74
|
"resource": resource,
|
|
71
75
|
"metadata": metadata,
|
|
@@ -75,7 +79,7 @@ class MockDatabase:
|
|
|
75
79
|
self.registrations.append(registration)
|
|
76
80
|
return registration
|
|
77
81
|
|
|
78
|
-
async def get_user_trigger_registrations_with_agents(self, user_id):
|
|
82
|
+
async def get_user_trigger_registrations_with_agents(self, user_id, tenant_id):
|
|
79
83
|
return [
|
|
80
84
|
{
|
|
81
85
|
**reg,
|
|
@@ -83,21 +87,39 @@ class MockDatabase:
|
|
|
83
87
|
"linked_agent_ids": reg.get("linked_agent_ids", []),
|
|
84
88
|
}
|
|
85
89
|
for reg in self.registrations
|
|
86
|
-
if reg["user_id"] == user_id
|
|
90
|
+
if reg["user_id"] == user_id and reg["tenant_id"] == tenant_id
|
|
87
91
|
]
|
|
88
92
|
|
|
89
|
-
async def get_trigger_registration(self, registration_id, user_id
|
|
93
|
+
async def get_trigger_registration(self, registration_id, user_id, tenant_id):
|
|
90
94
|
for reg in self.registrations:
|
|
91
95
|
if reg["id"] == registration_id:
|
|
92
|
-
if user_id
|
|
96
|
+
if reg["user_id"] == user_id and reg["tenant_id"] == tenant_id:
|
|
93
97
|
return reg
|
|
94
98
|
return None
|
|
95
99
|
|
|
96
100
|
async def find_user_registration_by_resource(
|
|
97
|
-
self, user_id, template_id, resource_data
|
|
101
|
+
self, user_id, tenant_id, template_id, resource_data
|
|
98
102
|
):
|
|
99
103
|
return None # No duplicates for testing
|
|
100
104
|
|
|
105
|
+
async def delete_trigger_registration(self, registration_id, user_id, tenant_id):
|
|
106
|
+
from fastapi import HTTPException
|
|
107
|
+
|
|
108
|
+
initial_count = len(self.registrations)
|
|
109
|
+
self.registrations = [
|
|
110
|
+
reg
|
|
111
|
+
for reg in self.registrations
|
|
112
|
+
if not (
|
|
113
|
+
reg["id"] == registration_id
|
|
114
|
+
and reg["user_id"] == user_id
|
|
115
|
+
and reg["tenant_id"] == tenant_id
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
deleted = len(self.registrations) < initial_count
|
|
119
|
+
if not deleted:
|
|
120
|
+
raise HTTPException(status_code=404, detail="Registration not found")
|
|
121
|
+
return deleted
|
|
122
|
+
|
|
101
123
|
async def link_agent_to_trigger(
|
|
102
124
|
self, agent_id, registration_id, created_by, field_selection=None
|
|
103
125
|
):
|
|
@@ -252,6 +274,7 @@ async def test_list_registrations_for_user(trigger_server):
|
|
|
252
274
|
# Create a registration
|
|
253
275
|
await trigger_server.database.create_trigger_registration(
|
|
254
276
|
user_id="test_user_token1",
|
|
277
|
+
tenant_id="test_tenant_token1",
|
|
255
278
|
template_id="test_trigger",
|
|
256
279
|
resource={"url": "https://example.com"},
|
|
257
280
|
metadata={"test": "value"},
|
|
@@ -277,6 +300,7 @@ async def test_link_agent_to_trigger(trigger_server):
|
|
|
277
300
|
# Create a registration first
|
|
278
301
|
reg = await trigger_server.database.create_trigger_registration(
|
|
279
302
|
user_id="test_user_token1",
|
|
303
|
+
tenant_id="test_tenant_token1",
|
|
280
304
|
template_id="test_trigger",
|
|
281
305
|
resource={"url": "https://example.com"},
|
|
282
306
|
metadata={},
|
|
@@ -304,6 +328,7 @@ async def test_unlink_agent_from_trigger(trigger_server):
|
|
|
304
328
|
# Create a registration and link an agent
|
|
305
329
|
reg = await trigger_server.database.create_trigger_registration(
|
|
306
330
|
user_id="test_user_token1",
|
|
331
|
+
tenant_id="test_tenant_token1",
|
|
307
332
|
template_id="test_trigger",
|
|
308
333
|
resource={"url": "https://example.com"},
|
|
309
334
|
metadata={},
|
|
@@ -333,12 +358,14 @@ async def test_user_isolation(trigger_server):
|
|
|
333
358
|
# Create registrations for two different users
|
|
334
359
|
await trigger_server.database.create_trigger_registration(
|
|
335
360
|
user_id="test_user_token1",
|
|
361
|
+
tenant_id="test_tenant_token1",
|
|
336
362
|
template_id="test_trigger",
|
|
337
363
|
resource={"url": "https://example.com"},
|
|
338
364
|
metadata={},
|
|
339
365
|
)
|
|
340
366
|
await trigger_server.database.create_trigger_registration(
|
|
341
367
|
user_id="test_user_token2",
|
|
368
|
+
tenant_id="test_tenant_token2",
|
|
342
369
|
template_id="test_trigger",
|
|
343
370
|
resource={"url": "https://other.com"},
|
|
344
371
|
metadata={},
|
|
@@ -368,8 +395,8 @@ async def test_user_isolation(trigger_server):
|
|
|
368
395
|
|
|
369
396
|
|
|
370
397
|
@pytest.mark.asyncio
|
|
371
|
-
async def
|
|
372
|
-
"""Test that
|
|
398
|
+
async def test_metadata_storage(trigger_server):
|
|
399
|
+
"""Test that handler metadata is properly stored."""
|
|
373
400
|
from langchain_triggers import TriggerRegistrationResult, TriggerTemplate
|
|
374
401
|
|
|
375
402
|
async def test_registration_handler(request, user_id, auth_client, registration):
|
|
@@ -394,7 +421,6 @@ async def test_client_metadata_storage(trigger_server):
|
|
|
394
421
|
json={
|
|
395
422
|
"type": "test_metadata_trigger",
|
|
396
423
|
"name": "test",
|
|
397
|
-
"metadata": {"tenant_id": "org-123"},
|
|
398
424
|
},
|
|
399
425
|
)
|
|
400
426
|
|
|
@@ -405,46 +431,70 @@ async def test_client_metadata_storage(trigger_server):
|
|
|
405
431
|
# Verify metadata in API response
|
|
406
432
|
registration_id = data["data"]["id"]
|
|
407
433
|
metadata = data["data"]["metadata"]
|
|
408
|
-
assert metadata["client_metadata"]["tenant_id"] == "org-123"
|
|
409
434
|
assert metadata["handler_data"] == "from_handler"
|
|
410
435
|
|
|
411
436
|
# Verify metadata persisted in database
|
|
412
437
|
db_registration = await trigger_server.database.get_trigger_registration(
|
|
413
|
-
registration_id, user_id="test_user_token1"
|
|
438
|
+
registration_id, user_id="test_user_token1", tenant_id="test_tenant_token1"
|
|
414
439
|
)
|
|
415
440
|
assert db_registration is not None
|
|
416
441
|
db_metadata = db_registration["metadata"]
|
|
417
|
-
assert db_metadata["client_metadata"]["tenant_id"] == "org-123"
|
|
418
442
|
assert db_metadata["handler_data"] == "from_handler"
|
|
419
443
|
|
|
420
444
|
|
|
421
445
|
@pytest.mark.asyncio
|
|
422
|
-
async def
|
|
423
|
-
"""Test
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
trigger_handler=dummy_trigger_handler,
|
|
446
|
+
async def test_tenant_isolation(trigger_server):
|
|
447
|
+
"""Test strict tenant isolation across all APIs."""
|
|
448
|
+
tenant_a_reg = await trigger_server.database.create_trigger_registration(
|
|
449
|
+
user_id="test_user_tenant_a",
|
|
450
|
+
tenant_id="test_tenant_tenant_a",
|
|
451
|
+
template_id="test_trigger",
|
|
452
|
+
resource={"url": "https://tenant-a.com"},
|
|
453
|
+
metadata={},
|
|
454
|
+
)
|
|
455
|
+
tenant_b_reg = await trigger_server.database.create_trigger_registration(
|
|
456
|
+
user_id="test_user_tenant_b",
|
|
457
|
+
tenant_id="test_tenant_tenant_b",
|
|
458
|
+
template_id="test_trigger",
|
|
459
|
+
resource={"url": "https://tenant-b.com"},
|
|
460
|
+
metadata={},
|
|
438
461
|
)
|
|
439
|
-
trigger_server.add_trigger(test_trigger)
|
|
440
462
|
|
|
441
463
|
transport = ASGITransport(app=trigger_server.app, raise_app_exceptions=True)
|
|
442
464
|
async with AsyncClient(base_url="http://localhost", transport=transport) as client:
|
|
465
|
+
response = await client.get(
|
|
466
|
+
"/v1/triggers/registrations", headers={"Authorization": "Bearer tenant_a"}
|
|
467
|
+
)
|
|
468
|
+
assert response.status_code == 200
|
|
469
|
+
data = response.json()
|
|
470
|
+
assert len(data["data"]) == 1
|
|
471
|
+
assert data["data"][0]["id"] == tenant_a_reg["id"]
|
|
472
|
+
|
|
473
|
+
response = await client.get(
|
|
474
|
+
"/v1/triggers/registrations", headers={"Authorization": "Bearer tenant_b"}
|
|
475
|
+
)
|
|
476
|
+
assert response.status_code == 200
|
|
477
|
+
data = response.json()
|
|
478
|
+
assert len(data["data"]) == 1
|
|
479
|
+
assert data["data"][0]["id"] == tenant_b_reg["id"]
|
|
480
|
+
|
|
443
481
|
response = await client.post(
|
|
444
|
-
"/v1/triggers/registrations",
|
|
445
|
-
headers={"Authorization": "Bearer
|
|
446
|
-
json={
|
|
482
|
+
f"/v1/triggers/registrations/{tenant_a_reg['id']}/agents/agent_x",
|
|
483
|
+
headers={"Authorization": "Bearer tenant_b"},
|
|
484
|
+
json={},
|
|
485
|
+
)
|
|
486
|
+
assert response.status_code == 404
|
|
487
|
+
|
|
488
|
+
response = await client.delete(
|
|
489
|
+
f"/v1/triggers/registrations/{tenant_a_reg['id']}",
|
|
490
|
+
headers={"Authorization": "Bearer tenant_b"},
|
|
447
491
|
)
|
|
492
|
+
assert response.status_code == 404
|
|
448
493
|
|
|
449
|
-
|
|
450
|
-
|
|
494
|
+
response = await client.delete(
|
|
495
|
+
f"/v1/triggers/registrations/{tenant_a_reg['id']}",
|
|
496
|
+
headers={"Authorization": "Bearer tenant_a"},
|
|
497
|
+
)
|
|
498
|
+
assert response.status_code == 200
|
|
499
|
+
data = response.json()
|
|
500
|
+
assert data["success"] is True
|
|
@@ -231,18 +231,6 @@ wheels = [
|
|
|
231
231
|
{ url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" },
|
|
232
232
|
]
|
|
233
233
|
|
|
234
|
-
[[package]]
|
|
235
|
-
name = "ecdsa"
|
|
236
|
-
version = "0.19.1"
|
|
237
|
-
source = { registry = "https://pypi.org/simple" }
|
|
238
|
-
dependencies = [
|
|
239
|
-
{ name = "six" },
|
|
240
|
-
]
|
|
241
|
-
sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" }
|
|
242
|
-
wheels = [
|
|
243
|
-
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
|
|
244
|
-
]
|
|
245
|
-
|
|
246
234
|
[[package]]
|
|
247
235
|
name = "fastapi"
|
|
248
236
|
version = "0.116.1"
|
|
@@ -393,7 +381,7 @@ wheels = [
|
|
|
393
381
|
|
|
394
382
|
[[package]]
|
|
395
383
|
name = "langchain-trigger-server"
|
|
396
|
-
version = "0.3.
|
|
384
|
+
version = "0.3.5"
|
|
397
385
|
source = { editable = "." }
|
|
398
386
|
dependencies = [
|
|
399
387
|
{ name = "apscheduler" },
|
|
@@ -404,7 +392,6 @@ dependencies = [
|
|
|
404
392
|
{ name = "langchain-auth" },
|
|
405
393
|
{ name = "langgraph-sdk" },
|
|
406
394
|
{ name = "pydantic" },
|
|
407
|
-
{ name = "python-jose", extra = ["cryptography"] },
|
|
408
395
|
{ name = "python-multipart" },
|
|
409
396
|
{ name = "supabase" },
|
|
410
397
|
{ name = "uvicorn", extra = ["standard"] },
|
|
@@ -442,7 +429,6 @@ requires-dist = [
|
|
|
442
429
|
{ name = "pydantic", specifier = ">=2.0.0" },
|
|
443
430
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
|
444
431
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
|
445
|
-
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" },
|
|
446
432
|
{ name = "python-multipart", specifier = ">=0.0.6" },
|
|
447
433
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
|
448
434
|
{ name = "supabase", specifier = ">=2.0.0" },
|
|
@@ -633,15 +619,6 @@ wheels = [
|
|
|
633
619
|
{ url = "https://files.pythonhosted.org/packages/a4/71/188a50ea64c17f73ff4df5196ec1553a8f1723421eb2d1069c73bab47d78/postgrest-1.1.1-py3-none-any.whl", hash = "sha256:98a6035ee1d14288484bfe36235942c5fb2d26af6d8120dfe3efbe007859251a", size = 22366, upload-time = "2025-06-23T19:21:33.637Z" },
|
|
634
620
|
]
|
|
635
621
|
|
|
636
|
-
[[package]]
|
|
637
|
-
name = "pyasn1"
|
|
638
|
-
version = "0.6.1"
|
|
639
|
-
source = { registry = "https://pypi.org/simple" }
|
|
640
|
-
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
|
|
641
|
-
wheels = [
|
|
642
|
-
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
|
|
643
|
-
]
|
|
644
|
-
|
|
645
622
|
[[package]]
|
|
646
623
|
name = "pycparser"
|
|
647
624
|
version = "2.23"
|
|
@@ -799,25 +776,6 @@ wheels = [
|
|
|
799
776
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
|
800
777
|
]
|
|
801
778
|
|
|
802
|
-
[[package]]
|
|
803
|
-
name = "python-jose"
|
|
804
|
-
version = "3.5.0"
|
|
805
|
-
source = { registry = "https://pypi.org/simple" }
|
|
806
|
-
dependencies = [
|
|
807
|
-
{ name = "ecdsa" },
|
|
808
|
-
{ name = "pyasn1" },
|
|
809
|
-
{ name = "rsa" },
|
|
810
|
-
]
|
|
811
|
-
sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" }
|
|
812
|
-
wheels = [
|
|
813
|
-
{ url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
|
|
814
|
-
]
|
|
815
|
-
|
|
816
|
-
[package.optional-dependencies]
|
|
817
|
-
cryptography = [
|
|
818
|
-
{ name = "cryptography" },
|
|
819
|
-
]
|
|
820
|
-
|
|
821
779
|
[[package]]
|
|
822
780
|
name = "python-multipart"
|
|
823
781
|
version = "0.0.20"
|
|
@@ -885,18 +843,6 @@ wheels = [
|
|
|
885
843
|
{ url = "https://files.pythonhosted.org/packages/d2/07/a5c7aef12f9a3497f5ad77157a37915645861e8b23b89b2ad4b0f11b48ad/realtime-2.7.0-py3-none-any.whl", hash = "sha256:d55a278803529a69d61c7174f16563a9cfa5bacc1664f656959694481903d99c", size = 22409, upload-time = "2025-07-28T18:54:21.383Z" },
|
|
886
844
|
]
|
|
887
845
|
|
|
888
|
-
[[package]]
|
|
889
|
-
name = "rsa"
|
|
890
|
-
version = "4.9.1"
|
|
891
|
-
source = { registry = "https://pypi.org/simple" }
|
|
892
|
-
dependencies = [
|
|
893
|
-
{ name = "pyasn1" },
|
|
894
|
-
]
|
|
895
|
-
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
|
896
|
-
wheels = [
|
|
897
|
-
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
|
898
|
-
]
|
|
899
|
-
|
|
900
846
|
[[package]]
|
|
901
847
|
name = "ruff"
|
|
902
848
|
version = "0.13.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Current: 0.3.6, PyPI: 0.3.5, Should publish: True
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"""Database module for trigger operations."""
|
|
2
|
-
|
|
3
|
-
from .interface import TriggerDatabaseInterface
|
|
4
|
-
from .supabase import SupabaseTriggerDatabase
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def create_database(database_type: str, **kwargs) -> TriggerDatabaseInterface:
|
|
8
|
-
"""Factory function to create database implementation."""
|
|
9
|
-
|
|
10
|
-
if database_type == "supabase":
|
|
11
|
-
return SupabaseTriggerDatabase(**kwargs)
|
|
12
|
-
else:
|
|
13
|
-
raise ValueError(f"Unknown database type: {database_type}")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
__all__ = ["TriggerDatabaseInterface", "SupabaseTriggerDatabase", "create_database"]
|
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
"""Supabase implementation of trigger database interface."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import hashlib
|
|
5
|
-
import logging
|
|
6
|
-
import os
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from cryptography.hazmat.backends import default_backend
|
|
10
|
-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
11
|
-
from supabase import create_client
|
|
12
|
-
|
|
13
|
-
from .interface import TriggerDatabaseInterface
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class SupabaseTriggerDatabase(TriggerDatabaseInterface):
|
|
19
|
-
"""Supabase implementation of trigger database operations."""
|
|
20
|
-
|
|
21
|
-
def __init__(self, supabase_url: str = None, supabase_key: str = None):
|
|
22
|
-
self.supabase_url = supabase_url or os.getenv("SUPABASE_URL")
|
|
23
|
-
self.supabase_key = supabase_key or os.getenv("SUPABASE_KEY")
|
|
24
|
-
|
|
25
|
-
if not self.supabase_url or not self.supabase_key:
|
|
26
|
-
raise ValueError(
|
|
27
|
-
"SUPABASE_URL and SUPABASE_KEY environment variables are required"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
self.client = create_client(self.supabase_url, self.supabase_key)
|
|
31
|
-
|
|
32
|
-
# Get encryption key for API key decryption - required
|
|
33
|
-
self.encryption_key = os.getenv("SECRETS_ENCRYPTION_KEY")
|
|
34
|
-
if not self.encryption_key:
|
|
35
|
-
raise ValueError("SECRETS_ENCRYPTION_KEY environment variable is required")
|
|
36
|
-
|
|
37
|
-
logger.info("Initialized SupabaseTriggerDatabase")
|
|
38
|
-
|
|
39
|
-
def _decrypt_secret(self, encrypted_secret: str) -> str:
|
|
40
|
-
"""Decrypt an encrypted secret using AES-256-GCM to match OAP Node.js implementation."""
|
|
41
|
-
try:
|
|
42
|
-
# Decode the base64 encoded encrypted data
|
|
43
|
-
combined = base64.b64decode(encrypted_secret)
|
|
44
|
-
|
|
45
|
-
# Constants from Node.js implementation
|
|
46
|
-
IV_LENGTH = 12 # 96 bits
|
|
47
|
-
TAG_LENGTH = 16 # 128 bits
|
|
48
|
-
|
|
49
|
-
# Minimum length check
|
|
50
|
-
if len(combined) < IV_LENGTH + TAG_LENGTH + 1:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
"Invalid encrypted secret format: too short or malformed"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
# Extract IV, encrypted data, and auth tag
|
|
56
|
-
iv = combined[:IV_LENGTH]
|
|
57
|
-
tag = combined[-TAG_LENGTH:]
|
|
58
|
-
encrypted_data = combined[IV_LENGTH:-TAG_LENGTH]
|
|
59
|
-
|
|
60
|
-
# Derive key using SHA-256 hash (same as Node.js deriveKey function)
|
|
61
|
-
key = hashlib.sha256(self.encryption_key.encode()).digest()
|
|
62
|
-
|
|
63
|
-
# Create AES-GCM cipher
|
|
64
|
-
cipher = Cipher(
|
|
65
|
-
algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend()
|
|
66
|
-
)
|
|
67
|
-
decryptor = cipher.decryptor()
|
|
68
|
-
|
|
69
|
-
# Decrypt the data
|
|
70
|
-
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
71
|
-
|
|
72
|
-
return decrypted_data.decode("utf-8")
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.error(f"Error decrypting secret: {e}")
|
|
75
|
-
raise ValueError("Failed to decrypt API key")
|
|
76
|
-
|
|
77
|
-
# ========== Trigger Templates ==========
|
|
78
|
-
|
|
79
|
-
async def create_trigger_template(
|
|
80
|
-
self,
|
|
81
|
-
id: str,
|
|
82
|
-
provider: str,
|
|
83
|
-
name: str,
|
|
84
|
-
description: str = None,
|
|
85
|
-
registration_schema: dict = None,
|
|
86
|
-
) -> dict[str, Any] | None:
|
|
87
|
-
"""Create a new trigger template."""
|
|
88
|
-
try:
|
|
89
|
-
data = {
|
|
90
|
-
"id": id,
|
|
91
|
-
"provider": provider,
|
|
92
|
-
"name": name,
|
|
93
|
-
"description": description,
|
|
94
|
-
"registration_schema": registration_schema or {},
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
response = self.client.table("trigger_templates").insert(data).execute()
|
|
98
|
-
return response.data[0] if response.data else None
|
|
99
|
-
|
|
100
|
-
except Exception as e:
|
|
101
|
-
logger.error(f"Error creating trigger template: {e}")
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
async def get_trigger_templates(self) -> list[dict[str, Any]]:
|
|
105
|
-
"""Get all available trigger templates."""
|
|
106
|
-
try:
|
|
107
|
-
response = self.client.table("trigger_templates").select("*").execute()
|
|
108
|
-
return response.data or []
|
|
109
|
-
except Exception as e:
|
|
110
|
-
logger.error(f"Error getting trigger templates: {e}")
|
|
111
|
-
return []
|
|
112
|
-
|
|
113
|
-
async def get_trigger_template(self, id: str) -> dict[str, Any] | None:
|
|
114
|
-
"""Get a specific trigger template by ID."""
|
|
115
|
-
try:
|
|
116
|
-
response = (
|
|
117
|
-
self.client.table("trigger_templates")
|
|
118
|
-
.select("*")
|
|
119
|
-
.eq("id", id)
|
|
120
|
-
.single()
|
|
121
|
-
.execute()
|
|
122
|
-
)
|
|
123
|
-
return response.data if response.data else None
|
|
124
|
-
except Exception as e:
|
|
125
|
-
# Don't log as error if template just doesn't exist (expected on first startup)
|
|
126
|
-
if (
|
|
127
|
-
"no rows returned" in str(e).lower()
|
|
128
|
-
or "multiple (or no) rows returned" in str(e).lower()
|
|
129
|
-
):
|
|
130
|
-
logger.debug(f"Trigger template {id} not found in database")
|
|
131
|
-
else:
|
|
132
|
-
logger.error(f"Error getting trigger template {id}: {e}")
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
# ========== Trigger Registrations ==========
|
|
136
|
-
|
|
137
|
-
async def create_trigger_registration(
|
|
138
|
-
self, user_id: str, template_id: str, resource: dict, metadata: dict = None
|
|
139
|
-
) -> dict[str, Any] | None:
|
|
140
|
-
"""Create a new trigger registration for a user."""
|
|
141
|
-
try:
|
|
142
|
-
# Verify template exists
|
|
143
|
-
template = await self.get_trigger_template(template_id)
|
|
144
|
-
if not template:
|
|
145
|
-
logger.error(f"Template not found for ID: {template_id}")
|
|
146
|
-
return None
|
|
147
|
-
|
|
148
|
-
data = {
|
|
149
|
-
"user_id": user_id,
|
|
150
|
-
"template_id": template_id,
|
|
151
|
-
"resource": resource,
|
|
152
|
-
"metadata": metadata or {},
|
|
153
|
-
"status": "active",
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
response = self.client.table("trigger_registrations").insert(data).execute()
|
|
157
|
-
return response.data[0] if response.data else None
|
|
158
|
-
|
|
159
|
-
except Exception as e:
|
|
160
|
-
logger.exception(f"Error creating trigger registration: {e}")
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
async def get_user_trigger_registrations(
|
|
164
|
-
self, user_id: str
|
|
165
|
-
) -> list[dict[str, Any]]:
|
|
166
|
-
"""Get all trigger registrations for a user."""
|
|
167
|
-
try:
|
|
168
|
-
response = (
|
|
169
|
-
self.client.table("trigger_registrations")
|
|
170
|
-
.select("""
|
|
171
|
-
*,
|
|
172
|
-
trigger_templates(id, name, description)
|
|
173
|
-
""")
|
|
174
|
-
.eq("user_id", user_id)
|
|
175
|
-
.order("created_at", desc=True)
|
|
176
|
-
.execute()
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
return response.data or []
|
|
180
|
-
except Exception as e:
|
|
181
|
-
logger.error(f"Error getting user trigger registrations: {e}")
|
|
182
|
-
return []
|
|
183
|
-
|
|
184
|
-
async def get_user_trigger_registrations_with_agents(
|
|
185
|
-
self, user_id: str
|
|
186
|
-
) -> list[dict[str, Any]]:
|
|
187
|
-
"""Get all trigger registrations for a user with linked agents in a single query."""
|
|
188
|
-
try:
|
|
189
|
-
response = (
|
|
190
|
-
self.client.table("trigger_registrations")
|
|
191
|
-
.select("""
|
|
192
|
-
*,
|
|
193
|
-
trigger_templates(id, name, description),
|
|
194
|
-
agent_trigger_links(agent_id)
|
|
195
|
-
""")
|
|
196
|
-
.eq("user_id", user_id)
|
|
197
|
-
.order("created_at", desc=True)
|
|
198
|
-
.execute()
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
# Process the results to extract linked agent IDs
|
|
202
|
-
if response.data:
|
|
203
|
-
for registration in response.data:
|
|
204
|
-
# Extract agent IDs from the agent_trigger_links
|
|
205
|
-
agent_links = registration.get("agent_trigger_links", [])
|
|
206
|
-
linked_agent_ids = [
|
|
207
|
-
link.get("agent_id")
|
|
208
|
-
for link in agent_links
|
|
209
|
-
if link.get("agent_id")
|
|
210
|
-
]
|
|
211
|
-
registration["linked_agent_ids"] = linked_agent_ids
|
|
212
|
-
|
|
213
|
-
# Clean up the raw agent_trigger_links data as it's no longer needed
|
|
214
|
-
registration.pop("agent_trigger_links", None)
|
|
215
|
-
|
|
216
|
-
return response.data or []
|
|
217
|
-
except Exception as e:
|
|
218
|
-
logger.error(f"Error getting user trigger registrations with agents: {e}")
|
|
219
|
-
return []
|
|
220
|
-
|
|
221
|
-
async def get_trigger_registration(
|
|
222
|
-
self, registration_id: str, user_id: str = None
|
|
223
|
-
) -> dict[str, Any] | None:
|
|
224
|
-
"""Get a specific trigger registration."""
|
|
225
|
-
try:
|
|
226
|
-
query = (
|
|
227
|
-
self.client.table("trigger_registrations")
|
|
228
|
-
.select("*")
|
|
229
|
-
.eq("id", registration_id)
|
|
230
|
-
)
|
|
231
|
-
if user_id:
|
|
232
|
-
query = query.eq("user_id", user_id)
|
|
233
|
-
|
|
234
|
-
response = query.single().execute()
|
|
235
|
-
return response.data if response.data else None
|
|
236
|
-
except Exception as e:
|
|
237
|
-
logger.error(f"Error getting trigger registration {registration_id}: {e}")
|
|
238
|
-
return None
|
|
239
|
-
|
|
240
|
-
async def update_trigger_metadata(
|
|
241
|
-
self, registration_id: str, metadata_updates: dict, user_id: str = None
|
|
242
|
-
) -> bool:
|
|
243
|
-
"""Update metadata for a trigger registration."""
|
|
244
|
-
try:
|
|
245
|
-
# Get current registration to merge metadata
|
|
246
|
-
current = await self.get_trigger_registration(registration_id, user_id)
|
|
247
|
-
if not current:
|
|
248
|
-
return False
|
|
249
|
-
|
|
250
|
-
# Merge existing metadata with updates
|
|
251
|
-
current_metadata = current.get("metadata", {})
|
|
252
|
-
updated_metadata = {**current_metadata, **metadata_updates}
|
|
253
|
-
|
|
254
|
-
query = (
|
|
255
|
-
self.client.table("trigger_registrations")
|
|
256
|
-
.update({"metadata": updated_metadata, "updated_at": "NOW()"})
|
|
257
|
-
.eq("id", registration_id)
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
if user_id:
|
|
261
|
-
query = query.eq("user_id", user_id)
|
|
262
|
-
|
|
263
|
-
response = query.execute()
|
|
264
|
-
return bool(response.data)
|
|
265
|
-
|
|
266
|
-
except Exception as e:
|
|
267
|
-
logger.error(f"Error updating trigger metadata: {e}")
|
|
268
|
-
return False
|
|
269
|
-
|
|
270
|
-
async def delete_trigger_registration(
|
|
271
|
-
self, registration_id: str, user_id: str = None
|
|
272
|
-
) -> bool:
|
|
273
|
-
"""Delete a trigger registration."""
|
|
274
|
-
try:
|
|
275
|
-
query = (
|
|
276
|
-
self.client.table("trigger_registrations")
|
|
277
|
-
.delete()
|
|
278
|
-
.eq("id", registration_id)
|
|
279
|
-
)
|
|
280
|
-
if user_id:
|
|
281
|
-
query = query.eq("user_id", user_id)
|
|
282
|
-
|
|
283
|
-
query.execute()
|
|
284
|
-
return True # Delete operations don't return data
|
|
285
|
-
|
|
286
|
-
except Exception as e:
|
|
287
|
-
logger.error(f"Error deleting trigger registration: {e}")
|
|
288
|
-
return False
|
|
289
|
-
|
|
290
|
-
async def find_registration_by_resource(
|
|
291
|
-
self, template_id: str, resource_data: dict[str, Any]
|
|
292
|
-
) -> dict[str, Any] | None:
|
|
293
|
-
"""Find trigger registration by matching resource data."""
|
|
294
|
-
try:
|
|
295
|
-
# Build query to match against trigger_registrations with template_id filter
|
|
296
|
-
query = (
|
|
297
|
-
self.client.table("trigger_registrations")
|
|
298
|
-
.select("*, trigger_templates(id, name, description)")
|
|
299
|
-
.eq("template_id", template_id)
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
# Add resource field matches
|
|
303
|
-
for field, value in resource_data.items():
|
|
304
|
-
query = query.eq(f"resource->>{field}", value)
|
|
305
|
-
|
|
306
|
-
response = query.execute()
|
|
307
|
-
|
|
308
|
-
if response.data:
|
|
309
|
-
return response.data[0] # Return first match
|
|
310
|
-
return None
|
|
311
|
-
|
|
312
|
-
except Exception as e:
|
|
313
|
-
logger.error(f"Error finding registration by resource: {e}")
|
|
314
|
-
return None
|
|
315
|
-
|
|
316
|
-
async def find_user_registration_by_resource(
|
|
317
|
-
self, user_id: str, template_id: str, resource_data: dict[str, Any]
|
|
318
|
-
) -> dict[str, Any] | None:
|
|
319
|
-
"""Find trigger registration by matching resource data for a specific user."""
|
|
320
|
-
try:
|
|
321
|
-
# Build query to match against trigger_registrations with template_id and user_id filter
|
|
322
|
-
query = (
|
|
323
|
-
self.client.table("trigger_registrations")
|
|
324
|
-
.select("*, trigger_templates(id, name, description)")
|
|
325
|
-
.eq("template_id", template_id)
|
|
326
|
-
.eq("user_id", user_id)
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
# Add resource field matches
|
|
330
|
-
for field, value in resource_data.items():
|
|
331
|
-
query = query.eq(f"resource->>{field}", value)
|
|
332
|
-
|
|
333
|
-
response = query.execute()
|
|
334
|
-
|
|
335
|
-
if response.data:
|
|
336
|
-
return response.data[0] # Return first match
|
|
337
|
-
return None
|
|
338
|
-
|
|
339
|
-
except Exception as e:
|
|
340
|
-
logger.error(f"Error finding user registration by resource: {e}")
|
|
341
|
-
return None
|
|
342
|
-
|
|
343
|
-
async def get_all_registrations(self, template_id: str) -> list[dict[str, Any]]:
|
|
344
|
-
"""Get all registrations for a specific trigger template."""
|
|
345
|
-
try:
|
|
346
|
-
response = (
|
|
347
|
-
self.client.table("trigger_registrations")
|
|
348
|
-
.select("*, trigger_templates(id, name, description)")
|
|
349
|
-
.eq("template_id", template_id)
|
|
350
|
-
.execute()
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
return response.data or []
|
|
354
|
-
except Exception as e:
|
|
355
|
-
logger.error(
|
|
356
|
-
f"Error getting all registrations for template {template_id}: {e}"
|
|
357
|
-
)
|
|
358
|
-
return []
|
|
359
|
-
|
|
360
|
-
# ========== Agent-Trigger Links ==========
|
|
361
|
-
|
|
362
|
-
async def link_agent_to_trigger(
|
|
363
|
-
self,
|
|
364
|
-
agent_id: str,
|
|
365
|
-
registration_id: str,
|
|
366
|
-
created_by: str,
|
|
367
|
-
field_selection: dict[str, bool] | None = None,
|
|
368
|
-
) -> bool:
|
|
369
|
-
"""Link an agent to a trigger registration with optional field selection."""
|
|
370
|
-
try:
|
|
371
|
-
data = {
|
|
372
|
-
"agent_id": agent_id,
|
|
373
|
-
"registration_id": registration_id,
|
|
374
|
-
"created_by": created_by,
|
|
375
|
-
"field_selection": field_selection,
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
response = self.client.table("agent_trigger_links").insert(data).execute()
|
|
379
|
-
return bool(response.data)
|
|
380
|
-
|
|
381
|
-
except Exception as e:
|
|
382
|
-
logger.error(f"Error linking agent to trigger: {e}")
|
|
383
|
-
return False
|
|
384
|
-
|
|
385
|
-
async def unlink_agent_from_trigger(
|
|
386
|
-
self, agent_id: str, registration_id: str
|
|
387
|
-
) -> bool:
|
|
388
|
-
"""Unlink an agent from a trigger registration."""
|
|
389
|
-
try:
|
|
390
|
-
(
|
|
391
|
-
self.client.table("agent_trigger_links")
|
|
392
|
-
.delete()
|
|
393
|
-
.eq("agent_id", agent_id)
|
|
394
|
-
.eq("registration_id", registration_id)
|
|
395
|
-
.execute()
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
return True # Delete operations don't return data
|
|
399
|
-
|
|
400
|
-
except Exception as e:
|
|
401
|
-
logger.error(f"Error unlinking agent from trigger: {e}")
|
|
402
|
-
return False
|
|
403
|
-
|
|
404
|
-
async def get_agents_for_trigger(
|
|
405
|
-
self, registration_id: str
|
|
406
|
-
) -> list[dict[str, Any]]:
|
|
407
|
-
"""Get all agent links for a trigger registration with field_selection."""
|
|
408
|
-
try:
|
|
409
|
-
response = (
|
|
410
|
-
self.client.table("agent_trigger_links")
|
|
411
|
-
.select("agent_id, field_selection")
|
|
412
|
-
.eq("registration_id", registration_id)
|
|
413
|
-
.execute()
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
return response.data or []
|
|
417
|
-
|
|
418
|
-
except Exception as e:
|
|
419
|
-
logger.error(f"Error getting agents for trigger: {e}")
|
|
420
|
-
return []
|
|
421
|
-
|
|
422
|
-
async def get_triggers_for_agent(self, agent_id: str) -> list[dict[str, Any]]:
|
|
423
|
-
"""Get all trigger registrations linked to an agent."""
|
|
424
|
-
try:
|
|
425
|
-
response = (
|
|
426
|
-
self.client.table("agent_trigger_links")
|
|
427
|
-
.select("""
|
|
428
|
-
registration_id,
|
|
429
|
-
trigger_registrations(
|
|
430
|
-
*,
|
|
431
|
-
trigger_templates(id, name, description)
|
|
432
|
-
)
|
|
433
|
-
""")
|
|
434
|
-
.eq("agent_id", agent_id)
|
|
435
|
-
.execute()
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
return [row["trigger_registrations"] for row in response.data or []]
|
|
439
|
-
|
|
440
|
-
except Exception as e:
|
|
441
|
-
logger.error(f"Error getting triggers for agent: {e}")
|
|
442
|
-
return []
|
|
443
|
-
|
|
444
|
-
# ========== Helper Methods ==========
|
|
445
|
-
|
|
446
|
-
async def get_user_from_token(self, token: str) -> str | None:
|
|
447
|
-
"""Extract user ID from JWT token via Supabase auth."""
|
|
448
|
-
try:
|
|
449
|
-
client = self._create_user_client(token)
|
|
450
|
-
response = client.auth.get_user(token)
|
|
451
|
-
return response.user.id if response.user else None
|
|
452
|
-
except Exception as e:
|
|
453
|
-
logger.error(f"Error getting user from token: {e}")
|
|
454
|
-
return None
|
|
455
|
-
|
|
456
|
-
async def get_user_by_email(self, email: str) -> str | None:
|
|
457
|
-
"""Get user ID by email from trigger registrations."""
|
|
458
|
-
try:
|
|
459
|
-
response = (
|
|
460
|
-
self.client.table("trigger_registrations")
|
|
461
|
-
.select("user_id")
|
|
462
|
-
.eq("resource->>email", email)
|
|
463
|
-
.limit(1)
|
|
464
|
-
.execute()
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
return response.data[0]["user_id"] if response.data else None
|
|
468
|
-
|
|
469
|
-
except Exception as e:
|
|
470
|
-
logger.error(f"Error getting user by email: {e}")
|
|
471
|
-
return None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Current: 0.3.4, PyPI: 0.3.3, Should publish: True
|
|
File without changes
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/_lint.yml
RENAMED
|
File without changes
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/_test.yml
RENAMED
|
File without changes
|
|
File without changes
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langchain_trigger_server-0.3.4 → langchain_trigger_server-0.3.6}/langchain_triggers/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|