apteva 0.4.17 → 0.4.19
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.
- package/dist/ActivityPage.9a1qg4bp.js +3 -0
- package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
- package/dist/App.1nmg2h01.js +4 -0
- package/dist/App.5qw2dtxs.js +4 -0
- package/dist/App.6nc5acvk.js +4 -0
- package/dist/App.7vzbaz56.js +4 -0
- package/dist/App.8rfz30p1.js +4 -0
- package/dist/App.amwp54wf.js +4 -0
- package/dist/App.e4202qb4.js +267 -0
- package/dist/App.errxz2q4.js +4 -0
- package/dist/App.f8qsyhpr.js +4 -0
- package/dist/App.g8vq68n0.js +20 -0
- package/dist/App.kfyrnznw.js +13 -0
- package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
- package/dist/App.p93mmyqw.js +4 -0
- package/dist/App.qmg33p02.js +4 -0
- package/dist/App.sdsc0258.js +4 -0
- package/dist/ConnectionsPage.7zqba1r0.js +3 -0
- package/dist/McpPage.kf2g327t.js +3 -0
- package/dist/SettingsPage.472c15ep.js +3 -0
- package/dist/SkillsPage.xdxnh68a.js +3 -0
- package/dist/TasksPage.7g0b8xwc.js +3 -0
- package/dist/TelemetryPage.pr7rbz4r.js +3 -0
- package/dist/TestsPage.zhc6rqjm.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -4
- package/src/auth/middleware.ts +2 -0
- package/src/channels/index.ts +40 -0
- package/src/channels/telegram.ts +306 -0
- package/src/db.ts +342 -11
- package/src/integrations/agentdojo.ts +1 -1
- package/src/mcp-handler.ts +31 -24
- package/src/mcp-platform.ts +41 -1
- package/src/providers.ts +22 -9
- package/src/routes/api/agent-utils.ts +38 -2
- package/src/routes/api/agents.ts +65 -2
- package/src/routes/api/channels.ts +182 -0
- package/src/routes/api/integrations.ts +13 -5
- package/src/routes/api/mcp.ts +27 -9
- package/src/routes/api/projects.ts +19 -2
- package/src/routes/api/system.ts +26 -12
- package/src/routes/api/telemetry.ts +30 -0
- package/src/routes/api/triggers.ts +478 -0
- package/src/routes/api/webhooks.ts +171 -0
- package/src/routes/api.ts +7 -1
- package/src/routes/static.ts +12 -3
- package/src/server.ts +43 -6
- package/src/triggers/agentdojo.ts +253 -0
- package/src/triggers/composio.ts +264 -0
- package/src/triggers/index.ts +71 -0
- package/src/tui/AgentList.tsx +145 -0
- package/src/tui/App.tsx +102 -0
- package/src/tui/Login.tsx +104 -0
- package/src/tui/api.ts +72 -0
- package/src/tui/index.tsx +7 -0
- package/src/web/App.tsx +18 -11
- package/src/web/components/agents/AgentCard.tsx +14 -7
- package/src/web/components/agents/AgentPanel.tsx +94 -137
- package/src/web/components/common/Icons.tsx +16 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/connections/ConnectionsPage.tsx +54 -0
- package/src/web/components/connections/IntegrationsTab.tsx +144 -0
- package/src/web/components/connections/OverviewTab.tsx +137 -0
- package/src/web/components/connections/TriggersTab.tsx +1169 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Header.tsx +196 -4
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
- package/src/web/components/settings/SettingsPage.tsx +364 -2
- package/src/web/components/tasks/TasksPage.tsx +2 -2
- package/src/web/components/tests/TestsPage.tsx +1 -2
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/context/index.ts +1 -1
- package/src/web/hooks/useAgents.ts +15 -11
- package/src/web/types.ts +1 -1
- package/dist/App.fq4xbpcz.js +0 -228
package/src/routes/static.ts
CHANGED
|
@@ -69,9 +69,18 @@ export async function serveStatic(req: Request, path: string): Promise<Response>
|
|
|
69
69
|
if (stat.isFile()) {
|
|
70
70
|
const file = Bun.file(fullPath);
|
|
71
71
|
const mimeType = getMimeType(filePath);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const headers: Record<string, string> = { "Content-Type": mimeType };
|
|
73
|
+
|
|
74
|
+
// Hashed assets (e.g. App.fq4xbpcz.js) are immutable — cache aggressively
|
|
75
|
+
// index.html must never be cached (it references the hashed assets)
|
|
76
|
+
const hasHash = /\.[a-z0-9]{6,}\.(js|css|map)$/.test(filePath);
|
|
77
|
+
if (hasHash) {
|
|
78
|
+
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
|
79
|
+
} else if (filePath !== "/index.html") {
|
|
80
|
+
headers["Cache-Control"] = "public, max-age=3600";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new Response(file, { headers });
|
|
75
84
|
}
|
|
76
85
|
} catch {
|
|
77
86
|
// Fall through to SPA handling
|
package/src/server.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { serveStatic } from "./routes/static";
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { mkdirSync, existsSync } from "fs";
|
|
8
|
-
import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, type McpServer, type Agent } from "./db";
|
|
8
|
+
import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, ChannelDB, type McpServer, type Agent } from "./db";
|
|
9
9
|
import { authMiddleware, type AuthContext } from "./auth/middleware";
|
|
10
10
|
import { startMcpProcess } from "./mcp-client";
|
|
11
11
|
import {
|
|
@@ -106,13 +106,18 @@ initDatabase(DATA_DIR);
|
|
|
106
106
|
// Initialize version tracking
|
|
107
107
|
initVersionTracking(DATA_DIR);
|
|
108
108
|
|
|
109
|
-
// Get agents
|
|
109
|
+
// Get agents, MCP servers, and channels that were running before restart (for auto-restart)
|
|
110
110
|
const agentsToRestart = AgentDB.findRunning();
|
|
111
111
|
const mcpServersToRestart = McpServerDB.findRunning();
|
|
112
|
+
const channelsToRestart = ChannelDB.findRunning();
|
|
112
113
|
|
|
113
114
|
// Reset all agents and MCP servers to stopped on startup (processes don't survive restart)
|
|
114
115
|
AgentDB.resetAllStatus();
|
|
115
116
|
McpServerDB.resetAllStatus();
|
|
117
|
+
// Reset channels too (bot polling doesn't survive restart)
|
|
118
|
+
for (const ch of channelsToRestart) {
|
|
119
|
+
ChannelDB.setStatus(ch.id, "stopped");
|
|
120
|
+
}
|
|
116
121
|
|
|
117
122
|
// Clean up orphaned processes on agent ports (targeted cleanup based on DB)
|
|
118
123
|
async function cleanupOrphanedProcesses(): Promise<void> {
|
|
@@ -167,7 +172,9 @@ export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = n
|
|
|
167
172
|
// Track agents currently being started (to prevent race conditions)
|
|
168
173
|
export const agentsStarting: Set<string> = new Set();
|
|
169
174
|
|
|
170
|
-
// Graceful shutdown handler - stop all
|
|
175
|
+
// Graceful shutdown handler - stop all agent processes when server exits
|
|
176
|
+
// NOTE: We intentionally do NOT update DB status here — agents marked "running"
|
|
177
|
+
// in the DB will be auto-restarted on next boot via findRunning() + resetAllStatus().
|
|
171
178
|
async function shutdownAllAgents() {
|
|
172
179
|
if (agentProcesses.size === 0) return;
|
|
173
180
|
|
|
@@ -182,7 +189,6 @@ async function shutdownAllAgents() {
|
|
|
182
189
|
}).catch(() => {});
|
|
183
190
|
|
|
184
191
|
proc.kill();
|
|
185
|
-
AgentDB.setStatus(agentId, "stopped");
|
|
186
192
|
} catch {
|
|
187
193
|
// Ignore errors during shutdown
|
|
188
194
|
}
|
|
@@ -192,15 +198,27 @@ async function shutdownAllAgents() {
|
|
|
192
198
|
|
|
193
199
|
// Handle process termination signals
|
|
194
200
|
let shuttingDown = false;
|
|
201
|
+
export function isShuttingDown(): boolean { return shuttingDown; }
|
|
202
|
+
async function shutdownAllChannels() {
|
|
203
|
+
try {
|
|
204
|
+
const { stopAllChannels } = await import("./channels");
|
|
205
|
+
await stopAllChannels();
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore import/stop errors during shutdown
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
process.on("SIGINT", async () => {
|
|
196
212
|
if (shuttingDown) return;
|
|
197
213
|
shuttingDown = true;
|
|
214
|
+
await shutdownAllChannels();
|
|
198
215
|
await shutdownAllAgents();
|
|
199
216
|
process.exit(0);
|
|
200
217
|
});
|
|
201
218
|
process.on("SIGTERM", async () => {
|
|
202
219
|
if (shuttingDown) return;
|
|
203
220
|
shuttingDown = true;
|
|
221
|
+
await shutdownAllChannels();
|
|
204
222
|
await shutdownAllAgents();
|
|
205
223
|
process.exit(0);
|
|
206
224
|
});
|
|
@@ -414,8 +432,8 @@ console.log(`
|
|
|
414
432
|
${c.darkGray}Click link or Cmd/Ctrl+C to copy${c.reset}
|
|
415
433
|
`);
|
|
416
434
|
|
|
417
|
-
// Auto-restart agents
|
|
418
|
-
const hasRestarts = agentsToRestart.length > 0 || mcpServersToRestart.length > 0;
|
|
435
|
+
// Auto-restart agents, MCP servers, and channels that were running before restart
|
|
436
|
+
const hasRestarts = agentsToRestart.length > 0 || mcpServersToRestart.length > 0 || channelsToRestart.length > 0;
|
|
419
437
|
|
|
420
438
|
if (hasRestarts) {
|
|
421
439
|
// Restart in background to not block startup
|
|
@@ -490,6 +508,25 @@ if (hasRestarts) {
|
|
|
490
508
|
}
|
|
491
509
|
}
|
|
492
510
|
}
|
|
511
|
+
|
|
512
|
+
// Restart channels (after agents, since channels depend on running agents)
|
|
513
|
+
if (channelsToRestart.length > 0) {
|
|
514
|
+
const { startChannel } = await import("./channels");
|
|
515
|
+
console.log(` ${c.darkGray}Channels${c.reset} ${c.gray}Restarting ${channelsToRestart.length} channel(s)...${c.reset}`);
|
|
516
|
+
|
|
517
|
+
for (const channel of channelsToRestart) {
|
|
518
|
+
try {
|
|
519
|
+
const result = await startChannel(channel.id);
|
|
520
|
+
if (result.success) {
|
|
521
|
+
console.log(` ${c.gray} ✓ ${channel.name} (${channel.type})${c.reset}`);
|
|
522
|
+
} else {
|
|
523
|
+
console.log(` ${c.gray} ✗ ${channel.name}: ${result.error}${c.reset}`);
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
console.log(` ${c.gray} ✗ ${channel.name}: ${err}${c.reset}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
493
530
|
})();
|
|
494
531
|
}
|
|
495
532
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// AgentDojo Trigger Provider
|
|
2
|
+
// Uses our MCP API's subscription and trigger system
|
|
3
|
+
// Docs: POST /subscribe, GET /subscriptions, GET /triggers
|
|
4
|
+
|
|
5
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
6
|
+
import type { TriggerProvider, TriggerType, TriggerInstance } from "./index";
|
|
7
|
+
|
|
8
|
+
const AGENTDOJO_API_BASE = process.env.AGENTDOJO_API_BASE || "https://api.agentdojo.dev";
|
|
9
|
+
|
|
10
|
+
function headers(apiKey: string) {
|
|
11
|
+
return { "X-API-Key": apiKey, "Content-Type": "application/json" };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
15
|
+
id: "agentdojo",
|
|
16
|
+
name: "AgentDojo",
|
|
17
|
+
|
|
18
|
+
async listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]> {
|
|
19
|
+
const params = new URLSearchParams({ is_active: "true", limit: "200" });
|
|
20
|
+
if (toolkitSlugs?.length) {
|
|
21
|
+
// Filter by toolkit name(s) — API supports one at a time, so fetch each
|
|
22
|
+
const allItems: any[] = [];
|
|
23
|
+
for (const slug of toolkitSlugs) {
|
|
24
|
+
const res = await fetch(
|
|
25
|
+
`${AGENTDOJO_API_BASE}/triggers?${new URLSearchParams({ toolkit_name: slug, is_active: "true", limit: "200" })}`,
|
|
26
|
+
{ headers: headers(apiKey) },
|
|
27
|
+
);
|
|
28
|
+
if (res.ok) {
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
const items = data.data || data.triggers || [];
|
|
31
|
+
allItems.push(...items);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return mapTriggerTypes(allItems);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/triggers?${params}`, {
|
|
38
|
+
headers: headers(apiKey),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
console.error("AgentDojo listTriggerTypes error:", res.status, await res.text());
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
const items = data.data || data.triggers || [];
|
|
48
|
+
return mapTriggerTypes(items);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null> {
|
|
52
|
+
const res = await fetch(
|
|
53
|
+
`${AGENTDOJO_API_BASE}/triggers/${encodeURIComponent(slug)}`,
|
|
54
|
+
{ headers: headers(apiKey) },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
if (res.status === 404) return null;
|
|
59
|
+
console.error("AgentDojo getTriggerType error:", res.status, await res.text());
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
const item = data.data || data;
|
|
65
|
+
return {
|
|
66
|
+
slug: item.slug,
|
|
67
|
+
name: item.display_name || item.event_name || item.slug,
|
|
68
|
+
description: item.description || "",
|
|
69
|
+
type: (item.mechanism as "webhook" | "poll") || "webhook",
|
|
70
|
+
toolkit_slug: item.toolkit?.name || item.toolkit_name || "",
|
|
71
|
+
toolkit_name: item.toolkit?.display_name || item.toolkit_display_name || "",
|
|
72
|
+
logo: item.toolkit?.icon_url || null,
|
|
73
|
+
config_schema: item.config_schema || {},
|
|
74
|
+
payload_schema: item.payload_schema || {},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async createTrigger(
|
|
79
|
+
apiKey: string,
|
|
80
|
+
slug: string,
|
|
81
|
+
connectedAccountId: string,
|
|
82
|
+
config?: Record<string, unknown>,
|
|
83
|
+
): Promise<{ triggerId: string }> {
|
|
84
|
+
// AgentDojo uses subscriptions — we create a subscription for this trigger
|
|
85
|
+
// The callback_url should be set in config or from stored webhook URL
|
|
86
|
+
const callbackUrl = (config?.callback_url as string) || "";
|
|
87
|
+
if (!callbackUrl) {
|
|
88
|
+
throw new Error("callback_url is required in config for AgentDojo triggers");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Separate known top-level fields from extra config (e.g. owner, repo for GitHub)
|
|
92
|
+
const { callback_url, title, events, server, prompt, agent_id, ...extraConfig } = config || {} as Record<string, unknown>;
|
|
93
|
+
|
|
94
|
+
const body: Record<string, unknown> = {
|
|
95
|
+
trigger_type_slug: slug,
|
|
96
|
+
credential_id: connectedAccountId,
|
|
97
|
+
callback_url: callbackUrl,
|
|
98
|
+
title: (title as string) || `Trigger: ${slug}`,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (events) body.events = events;
|
|
102
|
+
if (server) body.server = server;
|
|
103
|
+
if (prompt) body.prompt = prompt;
|
|
104
|
+
if (agent_id) body.agent_id = agent_id;
|
|
105
|
+
|
|
106
|
+
// Pass extra config fields (owner, repo, etc.) as the config object
|
|
107
|
+
// mcp-subscribe spreads this into the webhook register payload
|
|
108
|
+
if (Object.keys(extraConfig).length > 0) {
|
|
109
|
+
body.config = extraConfig;
|
|
110
|
+
console.log("AgentDojo createTrigger: extra config:", JSON.stringify(extraConfig));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscribe`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: headers(apiKey),
|
|
116
|
+
body: JSON.stringify(body),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const errText = await res.text();
|
|
121
|
+
console.error("AgentDojo createTrigger error:", res.status, errText);
|
|
122
|
+
throw new Error(`Failed to create AgentDojo subscription: ${errText}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = await res.json();
|
|
126
|
+
return { triggerId: String(data.subscription_id || data.id) };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async listTriggers(apiKey: string): Promise<TriggerInstance[]> {
|
|
130
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscriptions?status=active`, {
|
|
131
|
+
headers: headers(apiKey),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
console.error("AgentDojo listTriggers error:", res.status, await res.text());
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
const items = data.subscriptions || data.data || [];
|
|
141
|
+
|
|
142
|
+
return items.map((item: any) => ({
|
|
143
|
+
id: String(item.id || item.subscription_id),
|
|
144
|
+
trigger_slug: item.title || item.server || "",
|
|
145
|
+
connected_account_id: item.credential_id || null,
|
|
146
|
+
status: item.status === "active" ? "active" as const : "disabled" as const,
|
|
147
|
+
config: {
|
|
148
|
+
server: item.server,
|
|
149
|
+
events: item.events,
|
|
150
|
+
callback_url: item.callback_url,
|
|
151
|
+
prompt: item.prompt,
|
|
152
|
+
title: item.title,
|
|
153
|
+
},
|
|
154
|
+
created_at: item.created_at || "",
|
|
155
|
+
}));
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async enableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
159
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscription/update`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: headers(apiKey),
|
|
162
|
+
body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "active" }),
|
|
163
|
+
});
|
|
164
|
+
return res.ok;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async disableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
168
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/subscription/update`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: headers(apiKey),
|
|
171
|
+
body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "disabled" }),
|
|
172
|
+
});
|
|
173
|
+
return res.ok;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async deleteTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
177
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/unsubscribe`, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: headers(apiKey),
|
|
180
|
+
body: JSON.stringify({ subscription_id: parseInt(triggerId) }),
|
|
181
|
+
});
|
|
182
|
+
return res.ok;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }> {
|
|
186
|
+
// AgentDojo uses per-subscription callback URLs, not a global webhook.
|
|
187
|
+
// We just store the base URL locally — it will be passed when creating subscriptions.
|
|
188
|
+
// No remote API call needed.
|
|
189
|
+
return {};
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }> {
|
|
193
|
+
// AgentDojo doesn't have a global webhook config — each subscription has its own callback_url.
|
|
194
|
+
// Return null to indicate per-subscription mode.
|
|
195
|
+
return { url: null, secret: null };
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
verifyWebhook(req: Request, body: string, secret: string): boolean {
|
|
199
|
+
// AgentDojo forwards webhooks with an HMAC signature
|
|
200
|
+
const signature = req.headers.get("x-webhook-signature") || "";
|
|
201
|
+
if (!signature) {
|
|
202
|
+
// If no signature header, check for a simple shared token
|
|
203
|
+
const token = req.headers.get("x-webhook-token") || "";
|
|
204
|
+
return token === secret;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// HMAC-SHA256 verification: signature is "sha256=<hex>"
|
|
208
|
+
const sig = signature.startsWith("sha256=") ? signature.slice(7) : signature;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const expected = createHmac("sha256", secret).update(body).digest("hex");
|
|
212
|
+
return timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
parseWebhookPayload(body: Record<string, unknown>): {
|
|
219
|
+
triggerSlug: string;
|
|
220
|
+
triggerInstanceId: string | null;
|
|
221
|
+
payload: Record<string, unknown>;
|
|
222
|
+
} {
|
|
223
|
+
// AgentDojo forwarded payload format:
|
|
224
|
+
// { type: "webhook", server: "stripe-payments", event: "payment_intent.succeeded",
|
|
225
|
+
// data: { ... }, prompt: "...", subscription_id: 123, timestamp: "..." }
|
|
226
|
+
|
|
227
|
+
const server = (body.server as string) || "";
|
|
228
|
+
const event = (body.event as string) || "";
|
|
229
|
+
const triggerSlug = event ? `${server}:${event}` : server || (body.type as string) || "unknown";
|
|
230
|
+
|
|
231
|
+
const triggerInstanceId = body.subscription_id
|
|
232
|
+
? String(body.subscription_id)
|
|
233
|
+
: null;
|
|
234
|
+
|
|
235
|
+
const payload = (body.data as Record<string, unknown>) || {};
|
|
236
|
+
|
|
237
|
+
return { triggerSlug, triggerInstanceId, payload };
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
function mapTriggerTypes(items: any[]): TriggerType[] {
|
|
242
|
+
return items.map((item: any) => ({
|
|
243
|
+
slug: item.slug,
|
|
244
|
+
name: item.display_name || item.event_name || item.slug,
|
|
245
|
+
description: item.description || "",
|
|
246
|
+
type: (item.mechanism as "webhook" | "poll") || "webhook",
|
|
247
|
+
toolkit_slug: item.toolkit?.name || item.toolkit_name || "",
|
|
248
|
+
toolkit_name: item.toolkit?.display_name || item.toolkit_display_name || "",
|
|
249
|
+
logo: item.toolkit?.icon_url || null,
|
|
250
|
+
config_schema: item.config_schema || {},
|
|
251
|
+
payload_schema: item.payload_schema || {},
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Composio Trigger Provider
|
|
2
|
+
// https://docs.composio.dev/api-reference/triggers
|
|
3
|
+
|
|
4
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
5
|
+
import type { TriggerProvider, TriggerType, TriggerInstance } from "./index";
|
|
6
|
+
|
|
7
|
+
const COMPOSIO_API_BASE = "https://backend.composio.dev";
|
|
8
|
+
|
|
9
|
+
function headers(apiKey: string) {
|
|
10
|
+
return { "x-api-key": apiKey, "Content-Type": "application/json" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ComposioTriggerProvider: TriggerProvider = {
|
|
14
|
+
id: "composio",
|
|
15
|
+
name: "Composio",
|
|
16
|
+
|
|
17
|
+
async listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]> {
|
|
18
|
+
const params = new URLSearchParams();
|
|
19
|
+
if (toolkitSlugs?.length) {
|
|
20
|
+
params.set("toolkit_slugs", toolkitSlugs.join(","));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/triggers_types?${params}`, {
|
|
24
|
+
headers: headers(apiKey),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
console.error("Composio listTriggerTypes error:", res.status, await res.text());
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const items = data.items || data || [];
|
|
34
|
+
|
|
35
|
+
return items.map((item: any) => ({
|
|
36
|
+
slug: item.slug || item.enum,
|
|
37
|
+
name: item.name || item.slug,
|
|
38
|
+
description: item.description || "",
|
|
39
|
+
type: item.type || "webhook",
|
|
40
|
+
toolkit_slug: item.toolkit?.slug || item.app_slug || "",
|
|
41
|
+
toolkit_name: item.toolkit?.name || item.app_name || "",
|
|
42
|
+
logo: item.toolkit?.logo || item.logo || null,
|
|
43
|
+
config_schema: item.config || {},
|
|
44
|
+
payload_schema: item.payload || {},
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null> {
|
|
49
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/triggers_types/${encodeURIComponent(slug)}`, {
|
|
50
|
+
headers: headers(apiKey),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
if (res.status === 404) return null;
|
|
55
|
+
console.error("Composio getTriggerType error:", res.status, await res.text());
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const item = await res.json();
|
|
60
|
+
return {
|
|
61
|
+
slug: item.slug || item.enum,
|
|
62
|
+
name: item.name || item.slug,
|
|
63
|
+
description: item.description || "",
|
|
64
|
+
type: item.type || "webhook",
|
|
65
|
+
toolkit_slug: item.toolkit?.slug || item.app_slug || "",
|
|
66
|
+
toolkit_name: item.toolkit?.name || item.app_name || "",
|
|
67
|
+
logo: item.toolkit?.logo || item.logo || null,
|
|
68
|
+
config_schema: item.config || {},
|
|
69
|
+
payload_schema: item.payload || {},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async createTrigger(
|
|
74
|
+
apiKey: string,
|
|
75
|
+
slug: string,
|
|
76
|
+
connectedAccountId: string,
|
|
77
|
+
config?: Record<string, unknown>,
|
|
78
|
+
): Promise<{ triggerId: string }> {
|
|
79
|
+
const body: any = {
|
|
80
|
+
connected_account_id: connectedAccountId,
|
|
81
|
+
};
|
|
82
|
+
if (config) {
|
|
83
|
+
body.trigger_config = config;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const res = await fetch(
|
|
87
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/${encodeURIComponent(slug)}/upsert`,
|
|
88
|
+
{
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: headers(apiKey),
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const errText = await res.text();
|
|
97
|
+
console.error("Composio createTrigger error:", res.status, errText);
|
|
98
|
+
throw new Error(`Failed to create trigger: ${errText}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
return { triggerId: data.trigger_id || data.deprecated?.uuid || data.id };
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async listTriggers(apiKey: string): Promise<TriggerInstance[]> {
|
|
106
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/trigger_instances/active`, {
|
|
107
|
+
headers: headers(apiKey),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
console.error("Composio listTriggers error:", res.status, await res.text());
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
const items = data.items || data.triggers || data || [];
|
|
117
|
+
|
|
118
|
+
return items.map((item: any) => ({
|
|
119
|
+
id: item.id || item.trigger_id,
|
|
120
|
+
trigger_slug: item.trigger_name || item.trigger_slug || item.slug || "",
|
|
121
|
+
connected_account_id: item.connected_account_id || item.connectedAccountId || null,
|
|
122
|
+
status: item.disabled ? "disabled" : "active",
|
|
123
|
+
config: item.trigger_config || item.triggerConfig || {},
|
|
124
|
+
created_at: item.created_at || item.createdAt || "",
|
|
125
|
+
}));
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async enableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
129
|
+
const res = await fetch(
|
|
130
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/manage/${encodeURIComponent(triggerId)}`,
|
|
131
|
+
{
|
|
132
|
+
method: "PATCH",
|
|
133
|
+
headers: headers(apiKey),
|
|
134
|
+
body: JSON.stringify({ status: "enable" }),
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
return res.ok;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async disableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
141
|
+
const res = await fetch(
|
|
142
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/manage/${encodeURIComponent(triggerId)}`,
|
|
143
|
+
{
|
|
144
|
+
method: "PATCH",
|
|
145
|
+
headers: headers(apiKey),
|
|
146
|
+
body: JSON.stringify({ status: "disable" }),
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
return res.ok;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async deleteTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
153
|
+
const res = await fetch(
|
|
154
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/manage/${encodeURIComponent(triggerId)}`,
|
|
155
|
+
{
|
|
156
|
+
method: "DELETE",
|
|
157
|
+
headers: headers(apiKey),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
return res.ok;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }> {
|
|
164
|
+
// Use webhook_subscriptions API (requires HTTPS)
|
|
165
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/webhook_subscriptions`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: headers(apiKey),
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
webhook_url: webhookUrl,
|
|
170
|
+
enabled_events: ["composio.trigger.message"],
|
|
171
|
+
version: "V3",
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const errText = await res.text();
|
|
177
|
+
console.error("Composio setupWebhook error:", res.status, errText);
|
|
178
|
+
throw new Error(`Failed to set webhook URL: ${errText}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const data = await res.json();
|
|
182
|
+
const secret = data.secret || data.webhook_secret || undefined;
|
|
183
|
+
return { secret };
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }> {
|
|
187
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/webhook_subscriptions`, {
|
|
188
|
+
headers: headers(apiKey),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
console.error("Composio getWebhookConfig error:", res.status, await res.text());
|
|
193
|
+
return { url: null, secret: null };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
const items = data.items || data.subscriptions || (Array.isArray(data) ? data : [data]);
|
|
198
|
+
const sub = items[0];
|
|
199
|
+
return {
|
|
200
|
+
url: sub?.webhook_url || sub?.callback_url || sub?.url || null,
|
|
201
|
+
secret: sub?.secret || sub?.webhook_secret || null,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
verifyWebhook(req: Request, body: string, secret: string): boolean {
|
|
206
|
+
const signature = req.headers.get("webhook-signature") || "";
|
|
207
|
+
const webhookId = req.headers.get("webhook-id") || "";
|
|
208
|
+
const timestamp = req.headers.get("webhook-timestamp") || "";
|
|
209
|
+
|
|
210
|
+
if (!signature || !webhookId || !timestamp) return false;
|
|
211
|
+
|
|
212
|
+
// Check timestamp freshness (5-minute window to prevent replay)
|
|
213
|
+
const now = Math.floor(Date.now() / 1000);
|
|
214
|
+
const ts = parseInt(timestamp, 10);
|
|
215
|
+
if (isNaN(ts) || Math.abs(now - ts) > 300) return false;
|
|
216
|
+
|
|
217
|
+
// Composio signature: HMAC-SHA256("{webhook-id}.{timestamp}.{body}", secret)
|
|
218
|
+
const signedContent = `${webhookId}.${timestamp}.${body}`;
|
|
219
|
+
const expectedSignature = createHmac("sha256", secret)
|
|
220
|
+
.update(signedContent)
|
|
221
|
+
.digest("base64");
|
|
222
|
+
|
|
223
|
+
// Signature header format: "v1,<base64>"
|
|
224
|
+
const sig = signature.startsWith("v1,") ? signature.slice(3) : signature;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
return timingSafeEqual(
|
|
228
|
+
Buffer.from(sig, "base64"),
|
|
229
|
+
Buffer.from(expectedSignature, "base64"),
|
|
230
|
+
);
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
parseWebhookPayload(body: Record<string, unknown>): {
|
|
237
|
+
triggerSlug: string;
|
|
238
|
+
triggerInstanceId: string | null;
|
|
239
|
+
payload: Record<string, unknown>;
|
|
240
|
+
} {
|
|
241
|
+
// V3 format: { type: "composio.trigger.message", metadata: { trigger_slug, trigger_id, ... }, data: { ... } }
|
|
242
|
+
// V2/V1 format: { trigger_name, trigger_id, payload: { ... } }
|
|
243
|
+
const metadata = (body.metadata as Record<string, unknown>) || {};
|
|
244
|
+
const data = (body.data as Record<string, unknown>) || {};
|
|
245
|
+
|
|
246
|
+
const triggerSlug =
|
|
247
|
+
(metadata.trigger_slug as string) ||
|
|
248
|
+
(body.trigger_name as string) ||
|
|
249
|
+
((body.type as string) !== "composio.trigger.message" ? (body.type as string) : null) ||
|
|
250
|
+
"unknown";
|
|
251
|
+
|
|
252
|
+
const triggerInstanceId =
|
|
253
|
+
(metadata.trigger_id as string) ||
|
|
254
|
+
(body.trigger_id as string) ||
|
|
255
|
+
(body.triggerId as string) ||
|
|
256
|
+
null;
|
|
257
|
+
|
|
258
|
+
const payload =
|
|
259
|
+
(body.payload as Record<string, unknown>) ||
|
|
260
|
+
data;
|
|
261
|
+
|
|
262
|
+
return { triggerSlug, triggerInstanceId, payload };
|
|
263
|
+
},
|
|
264
|
+
};
|