apteva 0.4.17 → 0.4.18

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.
Files changed (59) hide show
  1. package/dist/ActivityPage.yv28a2vj.js +3 -0
  2. package/dist/ApiDocsPage.4ccwjjbk.js +4 -0
  3. package/dist/App.155wke5v.js +4 -0
  4. package/dist/App.2e19nvn4.js +13 -0
  5. package/dist/App.2ye1b5n0.js +4 -0
  6. package/dist/App.4da4ycbe.js +4 -0
  7. package/dist/App.b6wtzd1j.js +4 -0
  8. package/dist/App.fjrh28tf.js +4 -0
  9. package/dist/App.htc36cy8.js +4 -0
  10. package/dist/App.me6reaa6.js +4 -0
  11. package/dist/App.n5q6p960.js +4 -0
  12. package/dist/App.nft7h9jt.js +4 -0
  13. package/dist/App.np463xvy.js +4 -0
  14. package/dist/App.nps62kvt.js +4 -0
  15. package/dist/App.q8ws33cc.js +181 -0
  16. package/dist/App.tb0y0jmt.js +40 -0
  17. package/dist/ConnectionsPage.52evzrp7.js +3 -0
  18. package/dist/McpPage.bjqrp0n2.js +3 -0
  19. package/dist/SettingsPage.es76hnj2.js +3 -0
  20. package/dist/SkillsPage.06h8yf0h.js +3 -0
  21. package/dist/TasksPage.99df66mk.js +3 -0
  22. package/dist/TelemetryPage.bmdnxhq7.js +3 -0
  23. package/dist/TestsPage.denxrg8c.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +1 -1
  27. package/src/auth/middleware.ts +2 -0
  28. package/src/db.ts +162 -11
  29. package/src/mcp-platform.ts +41 -1
  30. package/src/routes/api/agent-utils.ts +38 -2
  31. package/src/routes/api/agents.ts +65 -2
  32. package/src/routes/api/projects.ts +19 -2
  33. package/src/routes/api/system.ts +26 -12
  34. package/src/routes/api/triggers.ts +458 -0
  35. package/src/routes/api/webhooks.ts +171 -0
  36. package/src/routes/api.ts +4 -0
  37. package/src/routes/static.ts +12 -3
  38. package/src/server.ts +4 -2
  39. package/src/triggers/agentdojo.ts +248 -0
  40. package/src/triggers/composio.ts +264 -0
  41. package/src/triggers/index.ts +71 -0
  42. package/src/web/App.tsx +17 -10
  43. package/src/web/components/agents/AgentCard.tsx +14 -7
  44. package/src/web/components/agents/AgentPanel.tsx +105 -115
  45. package/src/web/components/common/Icons.tsx +8 -0
  46. package/src/web/components/common/index.ts +1 -0
  47. package/src/web/components/connections/ConnectionsPage.tsx +54 -0
  48. package/src/web/components/connections/IntegrationsTab.tsx +144 -0
  49. package/src/web/components/connections/OverviewTab.tsx +183 -0
  50. package/src/web/components/connections/TriggersTab.tsx +690 -0
  51. package/src/web/components/index.ts +1 -0
  52. package/src/web/components/layout/Sidebar.tsx +7 -1
  53. package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
  54. package/src/web/components/settings/SettingsPage.tsx +96 -2
  55. package/src/web/components/tasks/TasksPage.tsx +2 -2
  56. package/src/web/components/tests/TestsPage.tsx +1 -2
  57. package/src/web/hooks/useAgents.ts +15 -11
  58. package/src/web/types.ts +1 -1
  59. package/dist/App.fq4xbpcz.js +0 -228
package/src/server.ts CHANGED
@@ -167,7 +167,9 @@ export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = n
167
167
  // Track agents currently being started (to prevent race conditions)
168
168
  export const agentsStarting: Set<string> = new Set();
169
169
 
170
- // Graceful shutdown handler - stop all agents when server exits
170
+ // Graceful shutdown handler - stop all agent processes when server exits
171
+ // NOTE: We intentionally do NOT update DB status here — agents marked "running"
172
+ // in the DB will be auto-restarted on next boot via findRunning() + resetAllStatus().
171
173
  async function shutdownAllAgents() {
172
174
  if (agentProcesses.size === 0) return;
173
175
 
@@ -182,7 +184,6 @@ async function shutdownAllAgents() {
182
184
  }).catch(() => {});
183
185
 
184
186
  proc.kill();
185
- AgentDB.setStatus(agentId, "stopped");
186
187
  } catch {
187
188
  // Ignore errors during shutdown
188
189
  }
@@ -192,6 +193,7 @@ async function shutdownAllAgents() {
192
193
 
193
194
  // Handle process termination signals
194
195
  let shuttingDown = false;
196
+ export function isShuttingDown(): boolean { return shuttingDown; }
195
197
  process.on("SIGINT", async () => {
196
198
  if (shuttingDown) return;
197
199
  shuttingDown = true;
@@ -0,0 +1,248 @@
1
+ // AgentDojo Trigger Provider
2
+ // Uses our MCP API's subscription and trigger system
3
+ // Docs: POST /mcp/subscribe, GET /mcp/subscriptions, GET /mcp/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}/mcp/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}/mcp/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}/mcp/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
+ const body: Record<string, unknown> = {
92
+ trigger_type_slug: slug,
93
+ credential_id: connectedAccountId,
94
+ callback_url: callbackUrl,
95
+ title: (config?.title as string) || `Trigger: ${slug}`,
96
+ };
97
+
98
+ if (config?.events) {
99
+ body.events = config.events;
100
+ }
101
+ if (config?.server) {
102
+ body.server = config.server;
103
+ }
104
+ if (config?.prompt) {
105
+ body.prompt = config.prompt;
106
+ }
107
+
108
+ const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscribe`, {
109
+ method: "POST",
110
+ headers: headers(apiKey),
111
+ body: JSON.stringify(body),
112
+ });
113
+
114
+ if (!res.ok) {
115
+ const errText = await res.text();
116
+ console.error("AgentDojo createTrigger error:", res.status, errText);
117
+ throw new Error(`Failed to create AgentDojo subscription: ${errText}`);
118
+ }
119
+
120
+ const data = await res.json();
121
+ return { triggerId: String(data.subscription_id || data.id) };
122
+ },
123
+
124
+ async listTriggers(apiKey: string): Promise<TriggerInstance[]> {
125
+ const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscriptions?status=active`, {
126
+ headers: headers(apiKey),
127
+ });
128
+
129
+ if (!res.ok) {
130
+ console.error("AgentDojo listTriggers error:", res.status, await res.text());
131
+ return [];
132
+ }
133
+
134
+ const data = await res.json();
135
+ const items = data.subscriptions || data.data || [];
136
+
137
+ return items.map((item: any) => ({
138
+ id: String(item.id || item.subscription_id),
139
+ trigger_slug: item.title || item.server || "",
140
+ connected_account_id: item.credential_id || null,
141
+ status: item.status === "active" ? "active" as const : "disabled" as const,
142
+ config: {
143
+ server: item.server,
144
+ events: item.events,
145
+ callback_url: item.callback_url,
146
+ prompt: item.prompt,
147
+ title: item.title,
148
+ },
149
+ created_at: item.created_at || "",
150
+ }));
151
+ },
152
+
153
+ async enableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
154
+ const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscription/update`, {
155
+ method: "POST",
156
+ headers: headers(apiKey),
157
+ body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "active" }),
158
+ });
159
+ return res.ok;
160
+ },
161
+
162
+ async disableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
163
+ const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscription/update`, {
164
+ method: "POST",
165
+ headers: headers(apiKey),
166
+ body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "disabled" }),
167
+ });
168
+ return res.ok;
169
+ },
170
+
171
+ async deleteTrigger(apiKey: string, triggerId: string): Promise<boolean> {
172
+ const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/unsubscribe`, {
173
+ method: "POST",
174
+ headers: headers(apiKey),
175
+ body: JSON.stringify({ subscription_id: parseInt(triggerId) }),
176
+ });
177
+ return res.ok;
178
+ },
179
+
180
+ async setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }> {
181
+ // AgentDojo uses per-subscription callback URLs, not a global webhook.
182
+ // We just store the base URL locally — it will be passed when creating subscriptions.
183
+ // No remote API call needed.
184
+ return {};
185
+ },
186
+
187
+ async getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }> {
188
+ // AgentDojo doesn't have a global webhook config — each subscription has its own callback_url.
189
+ // Return null to indicate per-subscription mode.
190
+ return { url: null, secret: null };
191
+ },
192
+
193
+ verifyWebhook(req: Request, body: string, secret: string): boolean {
194
+ // AgentDojo forwards webhooks with an HMAC signature
195
+ const signature = req.headers.get("x-webhook-signature") || "";
196
+ if (!signature) {
197
+ // If no signature header, check for a simple shared token
198
+ const token = req.headers.get("x-webhook-token") || "";
199
+ return token === secret;
200
+ }
201
+
202
+ // HMAC-SHA256 verification: signature is "sha256=<hex>"
203
+ const sig = signature.startsWith("sha256=") ? signature.slice(7) : signature;
204
+
205
+ try {
206
+ const expected = createHmac("sha256", secret).update(body).digest("hex");
207
+ return timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
208
+ } catch {
209
+ return false;
210
+ }
211
+ },
212
+
213
+ parseWebhookPayload(body: Record<string, unknown>): {
214
+ triggerSlug: string;
215
+ triggerInstanceId: string | null;
216
+ payload: Record<string, unknown>;
217
+ } {
218
+ // AgentDojo forwarded payload format:
219
+ // { type: "webhook", server: "stripe-payments", event: "payment_intent.succeeded",
220
+ // data: { ... }, prompt: "...", subscription_id: 123, timestamp: "..." }
221
+
222
+ const server = (body.server as string) || "";
223
+ const event = (body.event as string) || "";
224
+ const triggerSlug = event ? `${server}:${event}` : server || (body.type as string) || "unknown";
225
+
226
+ const triggerInstanceId = body.subscription_id
227
+ ? String(body.subscription_id)
228
+ : null;
229
+
230
+ const payload = (body.data as Record<string, unknown>) || {};
231
+
232
+ return { triggerSlug, triggerInstanceId, payload };
233
+ },
234
+ };
235
+
236
+ function mapTriggerTypes(items: any[]): TriggerType[] {
237
+ return items.map((item: any) => ({
238
+ slug: item.slug,
239
+ name: item.display_name || item.event_name || item.slug,
240
+ description: item.description || "",
241
+ type: (item.mechanism as "webhook" | "poll") || "webhook",
242
+ toolkit_slug: item.toolkit?.name || item.toolkit_name || "",
243
+ toolkit_name: item.toolkit?.display_name || item.toolkit_display_name || "",
244
+ logo: item.toolkit?.icon_url || null,
245
+ config_schema: item.config_schema || {},
246
+ payload_schema: item.payload_schema || {},
247
+ }));
248
+ }
@@ -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
+ };
@@ -0,0 +1,71 @@
1
+ // Generic Trigger Provider Interface
2
+ // Allows multiple providers (Composio, AgentDojo, local, etc.) to offer trigger/webhook integrations
3
+
4
+ export interface TriggerType {
5
+ slug: string;
6
+ name: string;
7
+ description: string;
8
+ type: "webhook" | "poll";
9
+ toolkit_slug: string;
10
+ toolkit_name: string;
11
+ logo: string | null;
12
+ config_schema: Record<string, unknown>;
13
+ payload_schema: Record<string, unknown>;
14
+ }
15
+
16
+ export interface TriggerInstance {
17
+ id: string;
18
+ trigger_slug: string;
19
+ connected_account_id: string | null;
20
+ status: "active" | "disabled";
21
+ config: Record<string, unknown>;
22
+ created_at: string;
23
+ }
24
+
25
+ export interface TriggerProvider {
26
+ id: string;
27
+ name: string;
28
+
29
+ // Browse available trigger types
30
+ listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]>;
31
+ getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null>;
32
+
33
+ // CRUD trigger instances (all remote)
34
+ createTrigger(
35
+ apiKey: string,
36
+ slug: string,
37
+ connectedAccountId: string,
38
+ config?: Record<string, unknown>,
39
+ ): Promise<{ triggerId: string }>;
40
+ listTriggers(apiKey: string): Promise<TriggerInstance[]>;
41
+ enableTrigger(apiKey: string, triggerId: string): Promise<boolean>;
42
+ disableTrigger(apiKey: string, triggerId: string): Promise<boolean>;
43
+ deleteTrigger(apiKey: string, triggerId: string): Promise<boolean>;
44
+
45
+ // Webhook configuration
46
+ setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }>;
47
+ getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }>;
48
+
49
+ // Webhook verification and parsing (each provider signs differently)
50
+ verifyWebhook(req: Request, body: string, secret: string): boolean;
51
+ parseWebhookPayload(body: Record<string, unknown>): {
52
+ triggerSlug: string;
53
+ triggerInstanceId: string | null;
54
+ payload: Record<string, unknown>;
55
+ };
56
+ }
57
+
58
+ // Provider registry
59
+ const triggerProviders: Map<string, TriggerProvider> = new Map();
60
+
61
+ export function registerTriggerProvider(provider: TriggerProvider) {
62
+ triggerProviders.set(provider.id, provider);
63
+ }
64
+
65
+ export function getTriggerProvider(id: string): TriggerProvider | undefined {
66
+ return triggerProviders.get(id);
67
+ }
68
+
69
+ export function getTriggerProviderIds(): string[] {
70
+ return Array.from(triggerProviders.keys());
71
+ }
package/src/web/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useMemo } from "react";
1
+ import React, { useState, useEffect, useMemo, lazy, Suspense } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import "@apteva/apteva-kit/styles.css";
4
4
 
@@ -12,28 +12,31 @@ import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects,
12
12
  // Hooks
13
13
  import { useAgents, useProviders, useOnboarding } from "./hooks";
14
14
 
15
- // Components
15
+ // Core components (always needed)
16
16
  import {
17
17
  LoadingSpinner,
18
18
  Header,
19
19
  Sidebar,
20
20
  ErrorBanner,
21
21
  OnboardingWizard,
22
- SettingsPage,
23
22
  CreateAgentModal,
24
23
  AgentsView,
25
24
  Dashboard,
26
- ActivityPage,
27
- TasksPage,
28
- McpPage,
29
- SkillsPage,
30
- TestsPage,
31
- TelemetryPage,
32
25
  LoginPage,
33
26
  } from "./components";
34
- import { ApiDocsPage } from "./components/api/ApiDocsPage";
35
27
  import { MetaAgentProvider, MetaAgentPanel } from "./components/meta-agent/MetaAgent";
36
28
 
29
+ // Lazy-loaded page components (only loaded when navigated to)
30
+ const SettingsPage = lazy(() => import("./components/settings/SettingsPage").then(m => ({ default: m.SettingsPage })));
31
+ const ActivityPage = lazy(() => import("./components/activity/ActivityPage").then(m => ({ default: m.ActivityPage })));
32
+ const TasksPage = lazy(() => import("./components/tasks/TasksPage").then(m => ({ default: m.TasksPage })));
33
+ const McpPage = lazy(() => import("./components/mcp/McpPage").then(m => ({ default: m.McpPage })));
34
+ const SkillsPage = lazy(() => import("./components/skills/SkillsPage").then(m => ({ default: m.SkillsPage })));
35
+ const TestsPage = lazy(() => import("./components/tests/TestsPage").then(m => ({ default: m.TestsPage })));
36
+ const TelemetryPage = lazy(() => import("./components/telemetry/TelemetryPage").then(m => ({ default: m.TelemetryPage })));
37
+ const ConnectionsPage = lazy(() => import("./components/connections/ConnectionsPage").then(m => ({ default: m.ConnectionsPage })));
38
+ const ApiDocsPage = lazy(() => import("./components/api/ApiDocsPage").then(m => ({ default: m.ApiDocsPage })));
39
+
37
40
  function AppContent() {
38
41
  // Auth state
39
42
  const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
@@ -254,6 +257,7 @@ function AppContent() {
254
257
  />
255
258
 
256
259
  <main className="flex-1 overflow-hidden flex">
260
+ <Suspense fallback={<LoadingSpinner />}>
257
261
  {route === "settings" && <SettingsPage />}
258
262
 
259
263
  {route === "activity" && (
@@ -292,6 +296,8 @@ function AppContent() {
292
296
 
293
297
  {route === "tasks" && <TasksPage />}
294
298
 
299
+ {route === "connections" && <ConnectionsPage />}
300
+
295
301
  {route === "mcp" && <McpPage />}
296
302
 
297
303
  {route === "skills" && <SkillsPage />}
@@ -301,6 +307,7 @@ function AppContent() {
301
307
  {route === "telemetry" && <TelemetryPage />}
302
308
 
303
309
  {route === "api" && <ApiDocsPage />}
310
+ </Suspense>
304
311
  </main>
305
312
  </div>
306
313