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
@@ -0,0 +1,458 @@
1
+ import { json } from "./helpers";
2
+ import { ProviderKeys } from "../../providers";
3
+ import { SubscriptionDB, SettingsDB } from "../../db";
4
+ import {
5
+ getTriggerProvider,
6
+ getTriggerProviderIds,
7
+ registerTriggerProvider,
8
+ } from "../../triggers";
9
+ import { ComposioTriggerProvider } from "../../triggers/composio";
10
+ import { AgentDojoTriggerProvider } from "../../triggers/agentdojo";
11
+ import type { AuthContext } from "../../auth/middleware";
12
+
13
+ // Register trigger providers on module load
14
+ registerTriggerProvider(ComposioTriggerProvider);
15
+ registerTriggerProvider(AgentDojoTriggerProvider);
16
+
17
+ export async function handleTriggerRoutes(
18
+ req: Request,
19
+ path: string,
20
+ method: string,
21
+ authContext?: AuthContext,
22
+ ): Promise<Response | null> {
23
+
24
+ // GET /api/settings/instance-url
25
+ if (path === "/api/settings/instance-url" && method === "GET") {
26
+ const url = SettingsDB.get("instance_url") || "";
27
+ return json({ instance_url: url });
28
+ }
29
+
30
+ // PUT /api/settings/instance-url
31
+ if (path === "/api/settings/instance-url" && method === "PUT") {
32
+ try {
33
+ const body = await req.json();
34
+ const { instance_url } = body;
35
+ if (instance_url) {
36
+ SettingsDB.set("instance_url", instance_url.replace(/\/+$/, "")); // strip trailing slash
37
+ } else {
38
+ SettingsDB.set("instance_url", "");
39
+ }
40
+ return json({ success: true, instance_url: SettingsDB.get("instance_url") });
41
+ } catch (e: any) {
42
+ return json({ error: e.message || "Failed to save instance URL" }, 500);
43
+ }
44
+ }
45
+
46
+ // GET /api/triggers/providers - List available trigger providers
47
+ if (path === "/api/triggers/providers" && method === "GET") {
48
+ const providerIds = getTriggerProviderIds();
49
+ const providers = providerIds.map(id => {
50
+ const provider = getTriggerProvider(id);
51
+ const hasKey = !!ProviderKeys.getDecrypted(id);
52
+ return { id, name: provider?.name || id, connected: hasKey };
53
+ });
54
+ return json({ providers });
55
+ }
56
+
57
+ // ============ Trigger Type Browsing ============
58
+
59
+ // GET /api/triggers/types?provider=composio&toolkit_slugs=github,gmail
60
+ if (path === "/api/triggers/types" && method === "GET") {
61
+ const url = new URL(req.url);
62
+ const providerId = url.searchParams.get("provider") || "composio";
63
+ const toolkitSlugsParam = url.searchParams.get("toolkit_slugs");
64
+ const toolkitSlugs = toolkitSlugsParam ? toolkitSlugsParam.split(",") : undefined;
65
+ const projectId = url.searchParams.get("project_id") || null;
66
+
67
+ const provider = getTriggerProvider(providerId);
68
+ if (!provider) {
69
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
70
+ }
71
+
72
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
73
+ if (!apiKey) {
74
+ return json({ error: `${provider.name} API key not configured`, types: [] }, 200);
75
+ }
76
+
77
+ try {
78
+ const types = await provider.listTriggerTypes(apiKey, toolkitSlugs);
79
+ return json({ types });
80
+ } catch (e) {
81
+ console.error(`Failed to list trigger types from ${providerId}:`, e);
82
+ return json({ error: "Failed to fetch trigger types" }, 500);
83
+ }
84
+ }
85
+
86
+ // GET /api/triggers/types/:slug?provider=composio
87
+ const typeMatch = path.match(/^\/api\/triggers\/types\/([^/]+)$/);
88
+ if (typeMatch && method === "GET") {
89
+ const slug = typeMatch[1];
90
+ const url = new URL(req.url);
91
+ const providerId = url.searchParams.get("provider") || "composio";
92
+ const projectId = url.searchParams.get("project_id") || null;
93
+
94
+ const provider = getTriggerProvider(providerId);
95
+ if (!provider) {
96
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
97
+ }
98
+
99
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
100
+ if (!apiKey) {
101
+ return json({ error: `${provider.name} API key not configured` }, 401);
102
+ }
103
+
104
+ try {
105
+ const triggerType = await provider.getTriggerType(apiKey, slug);
106
+ if (!triggerType) {
107
+ return json({ error: "Trigger type not found" }, 404);
108
+ }
109
+ return json({ type: triggerType });
110
+ } catch (e) {
111
+ console.error(`Failed to get trigger type ${slug}:`, e);
112
+ return json({ error: "Failed to fetch trigger type" }, 500);
113
+ }
114
+ }
115
+
116
+ // ============ Trigger Instance Management ============
117
+
118
+ // GET /api/triggers?provider=composio
119
+ if (path === "/api/triggers" && method === "GET") {
120
+ const url = new URL(req.url);
121
+ const providerId = url.searchParams.get("provider") || "composio";
122
+ const projectId = url.searchParams.get("project_id") || null;
123
+
124
+ const provider = getTriggerProvider(providerId);
125
+ if (!provider) {
126
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
127
+ }
128
+
129
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
130
+ if (!apiKey) {
131
+ return json({ error: `${provider.name} API key not configured`, triggers: [] }, 200);
132
+ }
133
+
134
+ try {
135
+ const triggers = await provider.listTriggers(apiKey);
136
+ return json({ triggers });
137
+ } catch (e) {
138
+ console.error(`Failed to list triggers from ${providerId}:`, e);
139
+ return json({ error: "Failed to fetch triggers" }, 500);
140
+ }
141
+ }
142
+
143
+ // POST /api/triggers?provider=composio
144
+ if (path === "/api/triggers" && method === "POST") {
145
+ const url = new URL(req.url);
146
+ const providerId = url.searchParams.get("provider") || "composio";
147
+ const projectId = url.searchParams.get("project_id") || null;
148
+
149
+ const provider = getTriggerProvider(providerId);
150
+ if (!provider) {
151
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
152
+ }
153
+
154
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
155
+ if (!apiKey) {
156
+ return json({ error: `${provider.name} API key not configured` }, 401);
157
+ }
158
+
159
+ try {
160
+ const body = await req.json();
161
+ const { slug, connectedAccountId, config } = body;
162
+
163
+ if (!slug || !connectedAccountId) {
164
+ return json({ error: "slug and connectedAccountId are required" }, 400);
165
+ }
166
+
167
+ const result = await provider.createTrigger(apiKey, slug, connectedAccountId, config);
168
+ return json(result, 201);
169
+ } catch (e: any) {
170
+ console.error(`Failed to create trigger:`, e);
171
+ return json({ error: e.message || "Failed to create trigger" }, 500);
172
+ }
173
+ }
174
+
175
+ // POST /api/triggers/:id/enable?provider=composio
176
+ const enableMatch = path.match(/^\/api\/triggers\/([^/]+)\/enable$/);
177
+ if (enableMatch && method === "POST") {
178
+ const triggerId = enableMatch[1];
179
+ const url = new URL(req.url);
180
+ const providerId = url.searchParams.get("provider") || "composio";
181
+ const projectId = url.searchParams.get("project_id") || null;
182
+
183
+ const provider = getTriggerProvider(providerId);
184
+ if (!provider) {
185
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
186
+ }
187
+
188
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
189
+ if (!apiKey) {
190
+ return json({ error: `${provider.name} API key not configured` }, 401);
191
+ }
192
+
193
+ try {
194
+ const success = await provider.enableTrigger(apiKey, triggerId);
195
+ return json({ success });
196
+ } catch (e) {
197
+ console.error(`Failed to enable trigger ${triggerId}:`, e);
198
+ return json({ error: "Failed to enable trigger" }, 500);
199
+ }
200
+ }
201
+
202
+ // POST /api/triggers/:id/disable?provider=composio
203
+ const disableMatch = path.match(/^\/api\/triggers\/([^/]+)\/disable$/);
204
+ if (disableMatch && method === "POST") {
205
+ const triggerId = disableMatch[1];
206
+ const url = new URL(req.url);
207
+ const providerId = url.searchParams.get("provider") || "composio";
208
+ const projectId = url.searchParams.get("project_id") || null;
209
+
210
+ const provider = getTriggerProvider(providerId);
211
+ if (!provider) {
212
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
213
+ }
214
+
215
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
216
+ if (!apiKey) {
217
+ return json({ error: `${provider.name} API key not configured` }, 401);
218
+ }
219
+
220
+ try {
221
+ const success = await provider.disableTrigger(apiKey, triggerId);
222
+ return json({ success });
223
+ } catch (e) {
224
+ console.error(`Failed to disable trigger ${triggerId}:`, e);
225
+ return json({ error: "Failed to disable trigger" }, 500);
226
+ }
227
+ }
228
+
229
+ // DELETE /api/triggers/:id?provider=composio
230
+ const deleteMatch = path.match(/^\/api\/triggers\/([^/]+)$/);
231
+ if (deleteMatch && method === "DELETE") {
232
+ const triggerId = deleteMatch[1];
233
+ const url = new URL(req.url);
234
+ const providerId = url.searchParams.get("provider") || "composio";
235
+ const projectId = url.searchParams.get("project_id") || null;
236
+
237
+ const provider = getTriggerProvider(providerId);
238
+ if (!provider) {
239
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
240
+ }
241
+
242
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
243
+ if (!apiKey) {
244
+ return json({ error: `${provider.name} API key not configured` }, 401);
245
+ }
246
+
247
+ try {
248
+ const success = await provider.deleteTrigger(apiKey, triggerId);
249
+ return json({ success });
250
+ } catch (e) {
251
+ console.error(`Failed to delete trigger ${triggerId}:`, e);
252
+ return json({ error: "Failed to delete trigger" }, 500);
253
+ }
254
+ }
255
+
256
+ // ============ Webhook Configuration ============
257
+
258
+ // POST /api/triggers/webhook/setup?provider=composio
259
+ if (path === "/api/triggers/webhook/setup" && method === "POST") {
260
+ const url = new URL(req.url);
261
+ const providerId = url.searchParams.get("provider") || "composio";
262
+ const projectId = url.searchParams.get("project_id") || null;
263
+
264
+ const provider = getTriggerProvider(providerId);
265
+ if (!provider) {
266
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
267
+ }
268
+
269
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
270
+ if (!apiKey) {
271
+ return json({ error: `${provider.name} API key not configured` }, 401);
272
+ }
273
+
274
+ try {
275
+ const body = await req.json();
276
+ const { webhookUrl } = body;
277
+
278
+ if (!webhookUrl) {
279
+ return json({ error: "webhookUrl is required" }, 400);
280
+ }
281
+
282
+ const result = await provider.setupWebhook(apiKey, webhookUrl);
283
+
284
+ // Store the webhook secret locally for HMAC verification
285
+ if (result.secret) {
286
+ SettingsDB.set(`${providerId}_webhook_secret`, result.secret);
287
+ }
288
+ // Store the webhook URL for reference
289
+ SettingsDB.set(`${providerId}_webhook_url`, webhookUrl);
290
+
291
+ return json({ success: true, ...result });
292
+ } catch (e: any) {
293
+ console.error(`Failed to setup webhook for ${providerId}:`, e);
294
+ return json({ error: e.message || "Failed to setup webhook" }, 500);
295
+ }
296
+ }
297
+
298
+ // GET /api/triggers/webhook/status?provider=composio
299
+ if (path === "/api/triggers/webhook/status" && method === "GET") {
300
+ const url = new URL(req.url);
301
+ const providerId = url.searchParams.get("provider") || "composio";
302
+ const projectId = url.searchParams.get("project_id") || null;
303
+
304
+ const provider = getTriggerProvider(providerId);
305
+ if (!provider) {
306
+ return json({ error: `Unknown trigger provider: ${providerId}` }, 404);
307
+ }
308
+
309
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
310
+ if (!apiKey) {
311
+ return json({ error: `${provider.name} API key not configured` }, 401);
312
+ }
313
+
314
+ try {
315
+ const config = await provider.getWebhookConfig(apiKey);
316
+ return json(config);
317
+ } catch (e) {
318
+ console.error(`Failed to get webhook status for ${providerId}:`, e);
319
+ return json({ error: "Failed to get webhook status" }, 500);
320
+ }
321
+ }
322
+
323
+ // ============ Subscription Management (local routing) ============
324
+
325
+ // GET /api/subscriptions?agent_id=xxx&project_id=xxx
326
+ if (path === "/api/subscriptions" && method === "GET") {
327
+ const url = new URL(req.url);
328
+ const agentId = url.searchParams.get("agent_id") || null;
329
+ const projectId = url.searchParams.get("project_id") || null;
330
+
331
+ let subscriptions;
332
+ if (agentId) {
333
+ subscriptions = SubscriptionDB.findByAgentId(agentId);
334
+ } else {
335
+ subscriptions = SubscriptionDB.findAll(projectId);
336
+ }
337
+
338
+ return json({ subscriptions });
339
+ }
340
+
341
+ // POST /api/subscriptions
342
+ if (path === "/api/subscriptions" && method === "POST") {
343
+ try {
344
+ const body = await req.json();
345
+ const { trigger_slug, trigger_instance_id, agent_id, project_id, public_url, provider: providerParam } = body;
346
+
347
+ if (!trigger_slug || !agent_id) {
348
+ return json({ error: "trigger_slug and agent_id are required" }, 400);
349
+ }
350
+
351
+ // Determine provider (default to composio for backward compat)
352
+ const providerId = providerParam || "composio";
353
+ const projId = project_id || null;
354
+ const provider = getTriggerProvider(providerId);
355
+ const apiKey = provider ? ProviderKeys.getDecryptedForProject(providerId, projId) : null;
356
+
357
+ if (provider && apiKey) {
358
+ const existingWebhook = SettingsDB.get(`${providerId}_webhook_url`);
359
+ if (!existingWebhook) {
360
+ try {
361
+ // Use instance_url setting first, then provided value, then request origin
362
+ const instanceUrl = SettingsDB.get("instance_url");
363
+ const origin = instanceUrl || public_url || new URL(req.url).origin;
364
+ const webhookUrl = `${origin}/api/webhooks/${providerId}`;
365
+ const result = await provider.setupWebhook(apiKey, webhookUrl);
366
+ if (result.secret) {
367
+ SettingsDB.set(`${providerId}_webhook_secret`, result.secret);
368
+ }
369
+ SettingsDB.set(`${providerId}_webhook_url`, webhookUrl);
370
+ console.log(`[subscriptions] Auto-configured ${providerId} webhook: ${webhookUrl}`);
371
+ } catch (e) {
372
+ console.warn(`[subscriptions] Failed to auto-setup ${providerId} webhook:`, e);
373
+ // Continue creating subscription — webhook can be set up manually later
374
+ }
375
+ }
376
+ }
377
+
378
+ const subscription = SubscriptionDB.create({
379
+ trigger_slug,
380
+ trigger_instance_id: trigger_instance_id || null,
381
+ agent_id,
382
+ enabled: true,
383
+ project_id: projId,
384
+ });
385
+
386
+ return json({ subscription }, 201);
387
+ } catch (e: any) {
388
+ console.error("Failed to create subscription:", e);
389
+ return json({ error: e.message || "Failed to create subscription" }, 500);
390
+ }
391
+ }
392
+
393
+ // GET /api/subscriptions/:id
394
+ const subGetMatch = path.match(/^\/api\/subscriptions\/([^/]+)$/);
395
+ if (subGetMatch && method === "GET") {
396
+ const subscription = SubscriptionDB.findById(subGetMatch[1]);
397
+ if (!subscription) {
398
+ return json({ error: "Subscription not found" }, 404);
399
+ }
400
+ return json({ subscription });
401
+ }
402
+
403
+ // PUT /api/subscriptions/:id
404
+ const subUpdateMatch = path.match(/^\/api\/subscriptions\/([^/]+)$/);
405
+ if (subUpdateMatch && method === "PUT") {
406
+ const id = subUpdateMatch[1];
407
+ const existing = SubscriptionDB.findById(id);
408
+ if (!existing) {
409
+ return json({ error: "Subscription not found" }, 404);
410
+ }
411
+
412
+ try {
413
+ const body = await req.json();
414
+ const updates: Record<string, unknown> = {};
415
+
416
+ if (body.trigger_slug !== undefined) updates.trigger_slug = body.trigger_slug;
417
+ if (body.trigger_instance_id !== undefined) updates.trigger_instance_id = body.trigger_instance_id;
418
+ if (body.agent_id !== undefined) updates.agent_id = body.agent_id;
419
+ if (body.enabled !== undefined) updates.enabled = body.enabled;
420
+
421
+ const updated = SubscriptionDB.update(id, updates);
422
+ return json({ subscription: updated });
423
+ } catch (e: any) {
424
+ console.error("Failed to update subscription:", e);
425
+ return json({ error: e.message || "Failed to update subscription" }, 500);
426
+ }
427
+ }
428
+
429
+ // DELETE /api/subscriptions/:id
430
+ const subDeleteMatch = path.match(/^\/api\/subscriptions\/([^/]+)$/);
431
+ if (subDeleteMatch && method === "DELETE") {
432
+ const success = SubscriptionDB.delete(subDeleteMatch[1]);
433
+ if (!success) {
434
+ return json({ error: "Subscription not found" }, 404);
435
+ }
436
+ return json({ success: true });
437
+ }
438
+
439
+ // POST /api/subscriptions/:id/enable
440
+ const subEnableMatch = path.match(/^\/api\/subscriptions\/([^/]+)\/enable$/);
441
+ if (subEnableMatch && method === "POST") {
442
+ const sub = SubscriptionDB.findById(subEnableMatch[1]);
443
+ if (!sub) return json({ error: "Subscription not found" }, 404);
444
+ const updated = SubscriptionDB.update(subEnableMatch[1], { enabled: true });
445
+ return json({ subscription: updated });
446
+ }
447
+
448
+ // POST /api/subscriptions/:id/disable
449
+ const subDisableMatch = path.match(/^\/api\/subscriptions\/([^/]+)\/disable$/);
450
+ if (subDisableMatch && method === "POST") {
451
+ const sub = SubscriptionDB.findById(subDisableMatch[1]);
452
+ if (!sub) return json({ error: "Subscription not found" }, 404);
453
+ const updated = SubscriptionDB.update(subDisableMatch[1], { enabled: false });
454
+ return json({ subscription: updated });
455
+ }
456
+
457
+ return null;
458
+ }
@@ -0,0 +1,171 @@
1
+ import { json } from "./helpers";
2
+ import { AgentDB, SubscriptionDB, SettingsDB } from "../../db";
3
+ import { getTriggerProvider } from "../../triggers";
4
+ import { agentFetch } from "./agent-utils";
5
+
6
+ /**
7
+ * Central webhook receiver for trigger providers.
8
+ * POST /api/webhooks/:provider — receives trigger events from any registered provider,
9
+ * verifies HMAC, looks up local subscriptions, and dispatches to the appropriate agent(s).
10
+ *
11
+ * This endpoint is public (HMAC-verified, no JWT/API key auth).
12
+ */
13
+ export async function handleWebhookRoutes(
14
+ req: Request,
15
+ path: string,
16
+ method: string,
17
+ ): Promise<Response | null> {
18
+
19
+ // POST /api/webhooks/composio
20
+ if (path === "/api/webhooks/composio" && method === "POST") {
21
+ return handleProviderWebhook(req, "composio");
22
+ }
23
+
24
+ // POST /api/webhooks/agentdojo
25
+ if (path === "/api/webhooks/agentdojo" && method === "POST") {
26
+ return handleProviderWebhook(req, "agentdojo");
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ async function handleProviderWebhook(req: Request, providerId: string): Promise<Response> {
33
+ const provider = getTriggerProvider(providerId);
34
+ if (!provider) {
35
+ return json({ error: `${providerId} provider not registered` }, 500);
36
+ }
37
+
38
+ // Read raw body for HMAC verification
39
+ let rawBody: string;
40
+ try {
41
+ rawBody = await req.text();
42
+ } catch {
43
+ return json({ error: "Failed to read request body" }, 400);
44
+ }
45
+
46
+ // Verify HMAC signature using stored webhook secret
47
+ const webhookSecret = SettingsDB.get(`${providerId}_webhook_secret`);
48
+ if (webhookSecret) {
49
+ const valid = provider.verifyWebhook(req, rawBody, webhookSecret);
50
+ if (!valid) {
51
+ console.warn(`[webhook] Invalid HMAC signature for ${providerId} webhook`);
52
+ return json({ error: "Invalid signature" }, 401);
53
+ }
54
+ } else {
55
+ console.warn(`[webhook] No ${providerId} webhook secret configured — skipping HMAC verification`);
56
+ }
57
+
58
+ // Parse the payload
59
+ let body: Record<string, unknown>;
60
+ try {
61
+ body = JSON.parse(rawBody);
62
+ } catch {
63
+ return json({ error: "Invalid JSON" }, 400);
64
+ }
65
+
66
+ // Log raw webhook for debugging
67
+ console.log(`[webhook:${providerId}] Raw payload:`, JSON.stringify(body, null, 2));
68
+
69
+ const { triggerSlug, triggerInstanceId, payload } = provider.parseWebhookPayload(body);
70
+ console.log(`[webhook:${providerId}] Parsed:`, { triggerSlug, triggerInstanceId });
71
+
72
+ // Respond 200 immediately — dispatch async
73
+ const dispatchPromise = dispatchToSubscribers(providerId, triggerSlug, triggerInstanceId, payload);
74
+
75
+ // Fire and forget — but log errors
76
+ dispatchPromise.catch(err => {
77
+ console.error(`[webhook:${providerId}] Dispatch error:`, err);
78
+ });
79
+
80
+ return json({ received: true, provider: providerId, trigger: triggerSlug });
81
+ }
82
+
83
+ async function dispatchToSubscribers(
84
+ providerId: string,
85
+ triggerSlug: string,
86
+ triggerInstanceId: string | null,
87
+ payload: Record<string, unknown>,
88
+ ): Promise<void> {
89
+ // Find matching subscriptions:
90
+ // 1. Exact match by trigger_instance_id (most specific)
91
+ // 2. Match by trigger_slug (broader)
92
+ let subscriptions = triggerInstanceId
93
+ ? SubscriptionDB.findByTriggerInstanceId(triggerInstanceId)
94
+ : [];
95
+
96
+ // If no instance-level matches, fall back to slug-level
97
+ if (subscriptions.length === 0) {
98
+ subscriptions = SubscriptionDB.findByTriggerSlug(triggerSlug);
99
+ }
100
+
101
+ // Filter to enabled only
102
+ subscriptions = subscriptions.filter(s => s.enabled);
103
+
104
+ if (subscriptions.length === 0) {
105
+ console.log(`[webhook:${providerId}] No subscriptions for trigger ${triggerSlug} (instance: ${triggerInstanceId || "none"})`);
106
+ return;
107
+ }
108
+
109
+ // Dispatch to each subscribed agent
110
+ const results = await Promise.allSettled(
111
+ subscriptions.map(sub => dispatchToAgent(sub.agent_id, triggerSlug, payload)),
112
+ );
113
+
114
+ for (let i = 0; i < results.length; i++) {
115
+ const result = results[i];
116
+ const sub = subscriptions[i];
117
+ if (result.status === "rejected") {
118
+ console.error(`[webhook:${providerId}] Failed to dispatch to agent ${sub.agent_id}:`, result.reason);
119
+ } else {
120
+ console.log(`[webhook:${providerId}] Dispatched ${triggerSlug} to agent ${sub.agent_id}: ${result.value}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ async function dispatchToAgent(
126
+ agentId: string,
127
+ triggerSlug: string,
128
+ payload: Record<string, unknown>,
129
+ ): Promise<string> {
130
+ const agent = AgentDB.findById(agentId);
131
+ if (!agent) {
132
+ return "agent_not_found";
133
+ }
134
+
135
+ if (agent.status !== "running" || !agent.port) {
136
+ return "agent_not_running";
137
+ }
138
+
139
+ // Format the trigger event as a chat message
140
+ const triggerName = triggerSlug.replace(/_/g, " ").replace(/:/g, " → ");
141
+ const message = [
142
+ `[Trigger: ${triggerName}]`,
143
+ "",
144
+ "```json",
145
+ JSON.stringify(payload, null, 2),
146
+ "```",
147
+ "",
148
+ "Process this event and take appropriate action.",
149
+ ].join("\n");
150
+
151
+ const response = await agentFetch(agent.id, agent.port, "/chat", {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ body: JSON.stringify({ message }),
155
+ });
156
+
157
+ // Consume the streaming response
158
+ if (response.body) {
159
+ try {
160
+ const reader = response.body.getReader();
161
+ while (true) {
162
+ const { done } = await reader.read();
163
+ if (done) break;
164
+ }
165
+ } catch {
166
+ // Ignore read errors
167
+ }
168
+ }
169
+
170
+ return response.ok ? "sent" : "agent_error";
171
+ }
package/src/routes/api.ts CHANGED
@@ -8,6 +8,8 @@ import { handleAgentRoutes } from "./api/agents";
8
8
  import { handleMcpRoutes } from "./api/mcp";
9
9
  import { handleSkillRoutes } from "./api/skills";
10
10
  import { handleIntegrationRoutes } from "./api/integrations";
11
+ import { handleTriggerRoutes } from "./api/triggers";
12
+ import { handleWebhookRoutes } from "./api/webhooks";
11
13
  import { handleMetaAgentRoutes } from "./api/meta-agent";
12
14
  import { handleTelemetryRoutes } from "./api/telemetry";
13
15
  import { handleTestRoutes } from "./api/tests";
@@ -30,6 +32,7 @@ export async function handleApiRequest(
30
32
  }
31
33
 
32
34
  return (
35
+ (await handleWebhookRoutes(req, path, method)) ?? // Public, HMAC-verified — before auth
33
36
  (await handleSystemRoutes(req, path, method, authContext)) ??
34
37
  (await handleApiKeyRoutes(req, path, method, authContext)) ?? // Must be before provider routes to handle /api/keys/personal
35
38
  (await handleProviderRoutes(req, path, method, authContext)) ??
@@ -39,6 +42,7 @@ export async function handleApiRequest(
39
42
  (await handleMcpRoutes(req, path, method)) ??
40
43
  (await handleSkillRoutes(req, path, method)) ??
41
44
  (await handleIntegrationRoutes(req, path, method)) ??
45
+ (await handleTriggerRoutes(req, path, method, authContext)) ??
42
46
  (await handleMetaAgentRoutes(req, path, method)) ??
43
47
  (await handleTelemetryRoutes(req, path, method)) ??
44
48
  (await handleTestRoutes(req, path, method)) ??
@@ -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
- return new Response(file, {
73
- headers: { "Content-Type": mimeType },
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