vibesuite 1.3.2 → 2.0.1
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/README.md +8 -1
- package/assets/.agent/skills/avoid-feature-creep/SKILL.md +307 -0
- package/assets/.agent/skills/avoid-feature-creep/agents/openai.yaml +3 -0
- package/assets/.agent/skills/avoid-feature-creep/assets/large-logo.png +0 -0
- package/assets/.agent/skills/avoid-feature-creep/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex/SKILL.md +62 -0
- package/assets/.agent/skills/convex/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-agents/SKILL.md +516 -0
- package/assets/.agent/skills/convex-agents/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-agents/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-agents/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-best-practices/SKILL.md +369 -0
- package/assets/.agent/skills/convex-best-practices/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-best-practices/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-best-practices/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-component-authoring/SKILL.md +457 -0
- package/assets/.agent/skills/convex-component-authoring/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-component-authoring/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-component-authoring/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-cron-jobs/SKILL.md +604 -0
- package/assets/.agent/skills/convex-cron-jobs/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-cron-jobs/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-cron-jobs/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-file-storage/SKILL.md +467 -0
- package/assets/.agent/skills/convex-file-storage/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-file-storage/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-file-storage/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-functions/SKILL.md +458 -0
- package/assets/.agent/skills/convex-functions/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-functions/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-functions/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-http-actions/SKILL.md +733 -0
- package/assets/.agent/skills/convex-http-actions/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-http-actions/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-http-actions/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-migrations/SKILL.md +712 -0
- package/assets/.agent/skills/convex-migrations/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-migrations/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-migrations/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-realtime/SKILL.md +443 -0
- package/assets/.agent/skills/convex-realtime/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-realtime/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-realtime/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-schema-validator/SKILL.md +400 -0
- package/assets/.agent/skills/convex-schema-validator/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-schema-validator/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-schema-validator/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-security-audit/SKILL.md +539 -0
- package/assets/.agent/skills/convex-security-audit/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-security-audit/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-security-audit/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/convex-security-check/SKILL.md +378 -0
- package/assets/.agent/skills/convex-security-check/agents/openai.yaml +3 -0
- package/assets/.agent/skills/convex-security-check/assets/large-logo.png +0 -0
- package/assets/.agent/skills/convex-security-check/assets/small-logo.svg +17 -0
- package/assets/.agent/skills/github-ops/SKILL.md +4 -4
- package/assets/.agent/skills/google-trends/SKILL.md +7 -7
- package/assets/.agent/skills/optimize-agent-context/SKILL.md +97 -0
- package/assets/.agent/skills/youtube-pipeline/SKILL.md +10 -10
- package/assets/.agent/workflows/LEGACY/init_smart_ops.md +2 -2
- package/assets/.agent/workflows/agent_reset.md +2 -2
- package/assets/.agent/workflows/mode-orchestrator.md +114 -640
- package/assets/.agent/workflows/mode-visionary.md +192 -0
- package/assets/.agent/workflows/optimize-agent-context.md +54 -0
- package/assets/.agent/workflows/remotion-build.md +17 -17
- package/assets/.agent/workflows/stitch.md +4 -4
- package/assets/VibeCode-Agents/custom_modes.yaml +1257 -0
- package/assets/VibeCode-Agents/vibe-orchestrator.yaml +427 -145
- package/assets/VibeCode-Agents/vibe-visionary.yaml +617 -0
- package/package.json +2 -2
- package/src/cli.js +416 -20
- package/src/harness.js +281 -0
- package/src/store.js +239 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: convex-http-actions
|
|
3
|
+
displayName: Convex HTTP Actions
|
|
4
|
+
description: External API integration and webhook handling including HTTP endpoint routing, request/response handling, authentication, CORS configuration, and webhook signature validation
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
author: Convex
|
|
7
|
+
tags: [convex, http, actions, webhooks, api, endpoints]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Convex HTTP Actions
|
|
11
|
+
|
|
12
|
+
Build HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications.
|
|
13
|
+
|
|
14
|
+
## Documentation Sources
|
|
15
|
+
|
|
16
|
+
Before implementing, do not assume; fetch the latest documentation:
|
|
17
|
+
|
|
18
|
+
- Primary: https://docs.convex.dev/functions/http-actions
|
|
19
|
+
- Actions Overview: https://docs.convex.dev/functions/actions
|
|
20
|
+
- Authentication: https://docs.convex.dev/auth
|
|
21
|
+
- For broader context: https://docs.convex.dev/llms.txt
|
|
22
|
+
|
|
23
|
+
## Instructions
|
|
24
|
+
|
|
25
|
+
### HTTP Actions Overview
|
|
26
|
+
|
|
27
|
+
HTTP actions allow you to define HTTP endpoints in Convex that can:
|
|
28
|
+
|
|
29
|
+
- Receive webhooks from third-party services
|
|
30
|
+
- Create custom API routes
|
|
31
|
+
- Handle file uploads
|
|
32
|
+
- Integrate with external services
|
|
33
|
+
- Serve dynamic content
|
|
34
|
+
|
|
35
|
+
### Basic HTTP Router Setup
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// convex/http.ts
|
|
39
|
+
import { httpRouter } from "convex/server";
|
|
40
|
+
import { httpAction } from "./_generated/server";
|
|
41
|
+
|
|
42
|
+
const http = httpRouter();
|
|
43
|
+
|
|
44
|
+
// Simple GET endpoint
|
|
45
|
+
http.route({
|
|
46
|
+
path: "/health",
|
|
47
|
+
method: "GET",
|
|
48
|
+
handler: httpAction(async (ctx, request) => {
|
|
49
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
});
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export default http;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Request Handling
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// convex/http.ts
|
|
63
|
+
import { httpRouter } from "convex/server";
|
|
64
|
+
import { httpAction } from "./_generated/server";
|
|
65
|
+
|
|
66
|
+
const http = httpRouter();
|
|
67
|
+
|
|
68
|
+
// Handle JSON body
|
|
69
|
+
http.route({
|
|
70
|
+
path: "/api/data",
|
|
71
|
+
method: "POST",
|
|
72
|
+
handler: httpAction(async (ctx, request) => {
|
|
73
|
+
// Parse JSON body
|
|
74
|
+
const body = await request.json();
|
|
75
|
+
|
|
76
|
+
// Access headers
|
|
77
|
+
const authHeader = request.headers.get("Authorization");
|
|
78
|
+
|
|
79
|
+
// Access URL parameters
|
|
80
|
+
const url = new URL(request.url);
|
|
81
|
+
const queryParam = url.searchParams.get("filter");
|
|
82
|
+
|
|
83
|
+
return new Response(
|
|
84
|
+
JSON.stringify({ received: body, filter: queryParam }),
|
|
85
|
+
{
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Handle form data
|
|
94
|
+
http.route({
|
|
95
|
+
path: "/api/form",
|
|
96
|
+
method: "POST",
|
|
97
|
+
handler: httpAction(async (ctx, request) => {
|
|
98
|
+
const formData = await request.formData();
|
|
99
|
+
const name = formData.get("name");
|
|
100
|
+
const email = formData.get("email");
|
|
101
|
+
|
|
102
|
+
return new Response(
|
|
103
|
+
JSON.stringify({ name, email }),
|
|
104
|
+
{
|
|
105
|
+
status: 200,
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Handle raw bytes
|
|
113
|
+
http.route({
|
|
114
|
+
path: "/api/upload",
|
|
115
|
+
method: "POST",
|
|
116
|
+
handler: httpAction(async (ctx, request) => {
|
|
117
|
+
const bytes = await request.bytes();
|
|
118
|
+
const contentType = request.headers.get("Content-Type") ?? "application/octet-stream";
|
|
119
|
+
|
|
120
|
+
// Store in Convex storage
|
|
121
|
+
const blob = new Blob([bytes], { type: contentType });
|
|
122
|
+
const storageId = await ctx.storage.store(blob);
|
|
123
|
+
|
|
124
|
+
return new Response(
|
|
125
|
+
JSON.stringify({ storageId }),
|
|
126
|
+
{
|
|
127
|
+
status: 200,
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export default http;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Path Parameters
|
|
138
|
+
|
|
139
|
+
Use path prefix matching for dynamic routes:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// convex/http.ts
|
|
143
|
+
import { httpRouter } from "convex/server";
|
|
144
|
+
import { httpAction } from "./_generated/server";
|
|
145
|
+
|
|
146
|
+
const http = httpRouter();
|
|
147
|
+
|
|
148
|
+
// Match /api/users/* with pathPrefix
|
|
149
|
+
http.route({
|
|
150
|
+
pathPrefix: "/api/users/",
|
|
151
|
+
method: "GET",
|
|
152
|
+
handler: httpAction(async (ctx, request) => {
|
|
153
|
+
const url = new URL(request.url);
|
|
154
|
+
// Extract user ID from path: /api/users/123 -> "123"
|
|
155
|
+
const userId = url.pathname.replace("/api/users/", "");
|
|
156
|
+
|
|
157
|
+
return new Response(
|
|
158
|
+
JSON.stringify({ userId }),
|
|
159
|
+
{
|
|
160
|
+
status: 200,
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export default http;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### CORS Configuration
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// convex/http.ts
|
|
174
|
+
import { httpRouter } from "convex/server";
|
|
175
|
+
import { httpAction } from "./_generated/server";
|
|
176
|
+
|
|
177
|
+
const http = httpRouter();
|
|
178
|
+
|
|
179
|
+
// CORS headers helper
|
|
180
|
+
const corsHeaders = {
|
|
181
|
+
"Access-Control-Allow-Origin": "*",
|
|
182
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
183
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
184
|
+
"Access-Control-Max-Age": "86400",
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Handle preflight requests
|
|
188
|
+
http.route({
|
|
189
|
+
path: "/api/data",
|
|
190
|
+
method: "OPTIONS",
|
|
191
|
+
handler: httpAction(async () => {
|
|
192
|
+
return new Response(null, {
|
|
193
|
+
status: 204,
|
|
194
|
+
headers: corsHeaders,
|
|
195
|
+
});
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Actual endpoint with CORS
|
|
200
|
+
http.route({
|
|
201
|
+
path: "/api/data",
|
|
202
|
+
method: "POST",
|
|
203
|
+
handler: httpAction(async (ctx, request) => {
|
|
204
|
+
const body = await request.json();
|
|
205
|
+
|
|
206
|
+
return new Response(
|
|
207
|
+
JSON.stringify({ success: true, data: body }),
|
|
208
|
+
{
|
|
209
|
+
status: 200,
|
|
210
|
+
headers: {
|
|
211
|
+
"Content-Type": "application/json",
|
|
212
|
+
...corsHeaders,
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export default http;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Webhook Handling
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// convex/http.ts
|
|
226
|
+
import { httpRouter } from "convex/server";
|
|
227
|
+
import { httpAction } from "./_generated/server";
|
|
228
|
+
import { internal } from "./_generated/api";
|
|
229
|
+
|
|
230
|
+
const http = httpRouter();
|
|
231
|
+
|
|
232
|
+
// Stripe webhook
|
|
233
|
+
http.route({
|
|
234
|
+
path: "/webhooks/stripe",
|
|
235
|
+
method: "POST",
|
|
236
|
+
handler: httpAction(async (ctx, request) => {
|
|
237
|
+
const signature = request.headers.get("stripe-signature");
|
|
238
|
+
if (!signature) {
|
|
239
|
+
return new Response("Missing signature", { status: 400 });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const body = await request.text();
|
|
243
|
+
|
|
244
|
+
// Verify webhook signature (in action with Node.js)
|
|
245
|
+
try {
|
|
246
|
+
await ctx.runAction(internal.stripe.verifyAndProcessWebhook, {
|
|
247
|
+
body,
|
|
248
|
+
signature,
|
|
249
|
+
});
|
|
250
|
+
return new Response("OK", { status: 200 });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error("Webhook error:", error);
|
|
253
|
+
return new Response("Webhook error", { status: 400 });
|
|
254
|
+
}
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// GitHub webhook
|
|
259
|
+
http.route({
|
|
260
|
+
path: "/webhooks/github",
|
|
261
|
+
method: "POST",
|
|
262
|
+
handler: httpAction(async (ctx, request) => {
|
|
263
|
+
const event = request.headers.get("X-GitHub-Event");
|
|
264
|
+
const signature = request.headers.get("X-Hub-Signature-256");
|
|
265
|
+
|
|
266
|
+
if (!signature) {
|
|
267
|
+
return new Response("Missing signature", { status: 400 });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const body = await request.text();
|
|
271
|
+
|
|
272
|
+
await ctx.runAction(internal.github.processWebhook, {
|
|
273
|
+
event: event ?? "unknown",
|
|
274
|
+
body,
|
|
275
|
+
signature,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return new Response("OK", { status: 200 });
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
export default http;
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Webhook Signature Verification
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// convex/stripe.ts
|
|
289
|
+
"use node";
|
|
290
|
+
|
|
291
|
+
import { internalAction, internalMutation } from "./_generated/server";
|
|
292
|
+
import { internal } from "./_generated/api";
|
|
293
|
+
import { v } from "convex/values";
|
|
294
|
+
import Stripe from "stripe";
|
|
295
|
+
|
|
296
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
297
|
+
|
|
298
|
+
export const verifyAndProcessWebhook = internalAction({
|
|
299
|
+
args: {
|
|
300
|
+
body: v.string(),
|
|
301
|
+
signature: v.string(),
|
|
302
|
+
},
|
|
303
|
+
returns: v.null(),
|
|
304
|
+
handler: async (ctx, args) => {
|
|
305
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
306
|
+
|
|
307
|
+
// Verify signature
|
|
308
|
+
const event = stripe.webhooks.constructEvent(
|
|
309
|
+
args.body,
|
|
310
|
+
args.signature,
|
|
311
|
+
webhookSecret
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Process based on event type
|
|
315
|
+
switch (event.type) {
|
|
316
|
+
case "checkout.session.completed":
|
|
317
|
+
await ctx.runMutation(internal.payments.handleCheckoutComplete, {
|
|
318
|
+
sessionId: event.data.object.id,
|
|
319
|
+
customerId: event.data.object.customer as string,
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
|
|
323
|
+
case "customer.subscription.updated":
|
|
324
|
+
await ctx.runMutation(internal.subscriptions.handleUpdate, {
|
|
325
|
+
subscriptionId: event.data.object.id,
|
|
326
|
+
status: event.data.object.status,
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Authentication in HTTP Actions
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// convex/http.ts
|
|
340
|
+
import { httpRouter } from "convex/server";
|
|
341
|
+
import { httpAction } from "./_generated/server";
|
|
342
|
+
import { internal } from "./_generated/api";
|
|
343
|
+
|
|
344
|
+
const http = httpRouter();
|
|
345
|
+
|
|
346
|
+
// API key authentication
|
|
347
|
+
http.route({
|
|
348
|
+
path: "/api/protected",
|
|
349
|
+
method: "GET",
|
|
350
|
+
handler: httpAction(async (ctx, request) => {
|
|
351
|
+
const apiKey = request.headers.get("X-API-Key");
|
|
352
|
+
|
|
353
|
+
if (!apiKey) {
|
|
354
|
+
return new Response(
|
|
355
|
+
JSON.stringify({ error: "Missing API key" }),
|
|
356
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Validate API key
|
|
361
|
+
const isValid = await ctx.runQuery(internal.auth.validateApiKey, {
|
|
362
|
+
apiKey,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (!isValid) {
|
|
366
|
+
return new Response(
|
|
367
|
+
JSON.stringify({ error: "Invalid API key" }),
|
|
368
|
+
{ status: 403, headers: { "Content-Type": "application/json" } }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Process authenticated request
|
|
373
|
+
const data = await ctx.runQuery(internal.data.getProtectedData, {});
|
|
374
|
+
|
|
375
|
+
return new Response(
|
|
376
|
+
JSON.stringify(data),
|
|
377
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
378
|
+
);
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Bearer token authentication
|
|
383
|
+
http.route({
|
|
384
|
+
path: "/api/user",
|
|
385
|
+
method: "GET",
|
|
386
|
+
handler: httpAction(async (ctx, request) => {
|
|
387
|
+
const authHeader = request.headers.get("Authorization");
|
|
388
|
+
|
|
389
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
390
|
+
return new Response(
|
|
391
|
+
JSON.stringify({ error: "Missing or invalid Authorization header" }),
|
|
392
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const token = authHeader.slice(7);
|
|
397
|
+
|
|
398
|
+
// Validate token and get user
|
|
399
|
+
const user = await ctx.runQuery(internal.auth.validateToken, { token });
|
|
400
|
+
|
|
401
|
+
if (!user) {
|
|
402
|
+
return new Response(
|
|
403
|
+
JSON.stringify({ error: "Invalid token" }),
|
|
404
|
+
{ status: 403, headers: { "Content-Type": "application/json" } }
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return new Response(
|
|
409
|
+
JSON.stringify(user),
|
|
410
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
411
|
+
);
|
|
412
|
+
}),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
export default http;
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Calling Mutations and Queries
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// convex/http.ts
|
|
422
|
+
import { httpRouter } from "convex/server";
|
|
423
|
+
import { httpAction } from "./_generated/server";
|
|
424
|
+
import { api, internal } from "./_generated/api";
|
|
425
|
+
|
|
426
|
+
const http = httpRouter();
|
|
427
|
+
|
|
428
|
+
http.route({
|
|
429
|
+
path: "/api/items",
|
|
430
|
+
method: "POST",
|
|
431
|
+
handler: httpAction(async (ctx, request) => {
|
|
432
|
+
const body = await request.json();
|
|
433
|
+
|
|
434
|
+
// Call a mutation
|
|
435
|
+
const itemId = await ctx.runMutation(internal.items.create, {
|
|
436
|
+
name: body.name,
|
|
437
|
+
description: body.description,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Query the created item
|
|
441
|
+
const item = await ctx.runQuery(internal.items.get, { id: itemId });
|
|
442
|
+
|
|
443
|
+
return new Response(
|
|
444
|
+
JSON.stringify(item),
|
|
445
|
+
{ status: 201, headers: { "Content-Type": "application/json" } }
|
|
446
|
+
);
|
|
447
|
+
}),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
http.route({
|
|
451
|
+
path: "/api/items",
|
|
452
|
+
method: "GET",
|
|
453
|
+
handler: httpAction(async (ctx, request) => {
|
|
454
|
+
const url = new URL(request.url);
|
|
455
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "10");
|
|
456
|
+
|
|
457
|
+
const items = await ctx.runQuery(internal.items.list, { limit });
|
|
458
|
+
|
|
459
|
+
return new Response(
|
|
460
|
+
JSON.stringify(items),
|
|
461
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
462
|
+
);
|
|
463
|
+
}),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
export default http;
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Error Handling
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
// convex/http.ts
|
|
473
|
+
import { httpRouter } from "convex/server";
|
|
474
|
+
import { httpAction } from "./_generated/server";
|
|
475
|
+
|
|
476
|
+
const http = httpRouter();
|
|
477
|
+
|
|
478
|
+
// Helper for JSON responses
|
|
479
|
+
function jsonResponse(data: unknown, status = 200) {
|
|
480
|
+
return new Response(JSON.stringify(data), {
|
|
481
|
+
status,
|
|
482
|
+
headers: { "Content-Type": "application/json" },
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Helper for error responses
|
|
487
|
+
function errorResponse(message: string, status: number) {
|
|
488
|
+
return jsonResponse({ error: message }, status);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
http.route({
|
|
492
|
+
path: "/api/process",
|
|
493
|
+
method: "POST",
|
|
494
|
+
handler: httpAction(async (ctx, request) => {
|
|
495
|
+
try {
|
|
496
|
+
// Validate content type
|
|
497
|
+
const contentType = request.headers.get("Content-Type");
|
|
498
|
+
if (!contentType?.includes("application/json")) {
|
|
499
|
+
return errorResponse("Content-Type must be application/json", 415);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Parse body
|
|
503
|
+
let body;
|
|
504
|
+
try {
|
|
505
|
+
body = await request.json();
|
|
506
|
+
} catch {
|
|
507
|
+
return errorResponse("Invalid JSON body", 400);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Validate required fields
|
|
511
|
+
if (!body.data) {
|
|
512
|
+
return errorResponse("Missing required field: data", 400);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Process request
|
|
516
|
+
const result = await ctx.runMutation(internal.process.handle, {
|
|
517
|
+
data: body.data,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return jsonResponse({ success: true, result }, 200);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.error("Processing error:", error);
|
|
523
|
+
return errorResponse("Internal server error", 500);
|
|
524
|
+
}
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
export default http;
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### File Downloads
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
// convex/http.ts
|
|
535
|
+
import { httpRouter } from "convex/server";
|
|
536
|
+
import { httpAction } from "./_generated/server";
|
|
537
|
+
import { Id } from "./_generated/dataModel";
|
|
538
|
+
|
|
539
|
+
const http = httpRouter();
|
|
540
|
+
|
|
541
|
+
http.route({
|
|
542
|
+
pathPrefix: "/files/",
|
|
543
|
+
method: "GET",
|
|
544
|
+
handler: httpAction(async (ctx, request) => {
|
|
545
|
+
const url = new URL(request.url);
|
|
546
|
+
const fileId = url.pathname.replace("/files/", "") as Id<"_storage">;
|
|
547
|
+
|
|
548
|
+
// Get file URL from storage
|
|
549
|
+
const fileUrl = await ctx.storage.getUrl(fileId);
|
|
550
|
+
|
|
551
|
+
if (!fileUrl) {
|
|
552
|
+
return new Response("File not found", { status: 404 });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Redirect to the file URL
|
|
556
|
+
return Response.redirect(fileUrl, 302);
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
export default http;
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
## Examples
|
|
564
|
+
|
|
565
|
+
### Complete Webhook Integration
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// convex/http.ts
|
|
569
|
+
import { httpRouter } from "convex/server";
|
|
570
|
+
import { httpAction } from "./_generated/server";
|
|
571
|
+
import { internal } from "./_generated/api";
|
|
572
|
+
|
|
573
|
+
const http = httpRouter();
|
|
574
|
+
|
|
575
|
+
// Clerk webhook for user sync
|
|
576
|
+
http.route({
|
|
577
|
+
path: "/webhooks/clerk",
|
|
578
|
+
method: "POST",
|
|
579
|
+
handler: httpAction(async (ctx, request) => {
|
|
580
|
+
const svixId = request.headers.get("svix-id");
|
|
581
|
+
const svixTimestamp = request.headers.get("svix-timestamp");
|
|
582
|
+
const svixSignature = request.headers.get("svix-signature");
|
|
583
|
+
|
|
584
|
+
if (!svixId || !svixTimestamp || !svixSignature) {
|
|
585
|
+
return new Response("Missing Svix headers", { status: 400 });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const body = await request.text();
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
await ctx.runAction(internal.clerk.verifyAndProcess, {
|
|
592
|
+
body,
|
|
593
|
+
svixId,
|
|
594
|
+
svixTimestamp,
|
|
595
|
+
svixSignature,
|
|
596
|
+
});
|
|
597
|
+
return new Response("OK", { status: 200 });
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error("Clerk webhook error:", error);
|
|
600
|
+
return new Response("Webhook verification failed", { status: 400 });
|
|
601
|
+
}
|
|
602
|
+
}),
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
export default http;
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
// convex/clerk.ts
|
|
610
|
+
"use node";
|
|
611
|
+
|
|
612
|
+
import { internalAction, internalMutation } from "./_generated/server";
|
|
613
|
+
import { internal } from "./_generated/api";
|
|
614
|
+
import { v } from "convex/values";
|
|
615
|
+
import { Webhook } from "svix";
|
|
616
|
+
|
|
617
|
+
export const verifyAndProcess = internalAction({
|
|
618
|
+
args: {
|
|
619
|
+
body: v.string(),
|
|
620
|
+
svixId: v.string(),
|
|
621
|
+
svixTimestamp: v.string(),
|
|
622
|
+
svixSignature: v.string(),
|
|
623
|
+
},
|
|
624
|
+
returns: v.null(),
|
|
625
|
+
handler: async (ctx, args) => {
|
|
626
|
+
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!;
|
|
627
|
+
const wh = new Webhook(webhookSecret);
|
|
628
|
+
|
|
629
|
+
const event = wh.verify(args.body, {
|
|
630
|
+
"svix-id": args.svixId,
|
|
631
|
+
"svix-timestamp": args.svixTimestamp,
|
|
632
|
+
"svix-signature": args.svixSignature,
|
|
633
|
+
}) as { type: string; data: Record<string, unknown> };
|
|
634
|
+
|
|
635
|
+
switch (event.type) {
|
|
636
|
+
case "user.created":
|
|
637
|
+
await ctx.runMutation(internal.users.create, {
|
|
638
|
+
clerkId: event.data.id as string,
|
|
639
|
+
email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address,
|
|
640
|
+
name: `${event.data.first_name} ${event.data.last_name}`,
|
|
641
|
+
});
|
|
642
|
+
break;
|
|
643
|
+
|
|
644
|
+
case "user.updated":
|
|
645
|
+
await ctx.runMutation(internal.users.update, {
|
|
646
|
+
clerkId: event.data.id as string,
|
|
647
|
+
email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address,
|
|
648
|
+
name: `${event.data.first_name} ${event.data.last_name}`,
|
|
649
|
+
});
|
|
650
|
+
break;
|
|
651
|
+
|
|
652
|
+
case "user.deleted":
|
|
653
|
+
await ctx.runMutation(internal.users.remove, {
|
|
654
|
+
clerkId: event.data.id as string,
|
|
655
|
+
});
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return null;
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Schema for HTTP API
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
// convex/schema.ts
|
|
668
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
669
|
+
import { v } from "convex/values";
|
|
670
|
+
|
|
671
|
+
export default defineSchema({
|
|
672
|
+
apiKeys: defineTable({
|
|
673
|
+
key: v.string(),
|
|
674
|
+
userId: v.id("users"),
|
|
675
|
+
name: v.string(),
|
|
676
|
+
createdAt: v.number(),
|
|
677
|
+
lastUsedAt: v.optional(v.number()),
|
|
678
|
+
revokedAt: v.optional(v.number()),
|
|
679
|
+
})
|
|
680
|
+
.index("by_key", ["key"])
|
|
681
|
+
.index("by_user", ["userId"]),
|
|
682
|
+
|
|
683
|
+
webhookEvents: defineTable({
|
|
684
|
+
source: v.string(),
|
|
685
|
+
eventType: v.string(),
|
|
686
|
+
payload: v.any(),
|
|
687
|
+
processedAt: v.number(),
|
|
688
|
+
status: v.union(
|
|
689
|
+
v.literal("success"),
|
|
690
|
+
v.literal("failed")
|
|
691
|
+
),
|
|
692
|
+
error: v.optional(v.string()),
|
|
693
|
+
})
|
|
694
|
+
.index("by_source", ["source"])
|
|
695
|
+
.index("by_status", ["status"]),
|
|
696
|
+
|
|
697
|
+
users: defineTable({
|
|
698
|
+
clerkId: v.string(),
|
|
699
|
+
email: v.string(),
|
|
700
|
+
name: v.string(),
|
|
701
|
+
}).index("by_clerk_id", ["clerkId"]),
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## Best Practices
|
|
706
|
+
|
|
707
|
+
- Never run `npx convex deploy` unless explicitly instructed
|
|
708
|
+
- Never run any git commands unless explicitly instructed
|
|
709
|
+
- Always validate and sanitize incoming request data
|
|
710
|
+
- Use internal functions for database operations
|
|
711
|
+
- Implement proper error handling with appropriate status codes
|
|
712
|
+
- Add CORS headers for browser-accessible endpoints
|
|
713
|
+
- Verify webhook signatures before processing
|
|
714
|
+
- Log webhook events for debugging
|
|
715
|
+
- Use environment variables for secrets
|
|
716
|
+
- Handle timeouts gracefully
|
|
717
|
+
|
|
718
|
+
## Common Pitfalls
|
|
719
|
+
|
|
720
|
+
1. **Missing CORS preflight handler** - Browsers send OPTIONS requests first
|
|
721
|
+
2. **Not validating webhook signatures** - Security vulnerability
|
|
722
|
+
3. **Exposing internal functions** - Use internal functions from HTTP actions
|
|
723
|
+
4. **Forgetting Content-Type headers** - Clients may not parse responses correctly
|
|
724
|
+
5. **Not handling request body errors** - Invalid JSON will throw
|
|
725
|
+
6. **Blocking on long operations** - Use scheduled functions for heavy processing
|
|
726
|
+
|
|
727
|
+
## References
|
|
728
|
+
|
|
729
|
+
- Convex Documentation: https://docs.convex.dev/
|
|
730
|
+
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
|
|
731
|
+
- HTTP Actions: https://docs.convex.dev/functions/http-actions
|
|
732
|
+
- Actions: https://docs.convex.dev/functions/actions
|
|
733
|
+
- Authentication: https://docs.convex.dev/auth
|