@fatagnus/convex-feedback 0.2.7 → 0.2.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fatagnus/convex-feedback
2
2
 
3
- Bug reports and feedback collection component for Convex applications with AI analysis and email notifications.
3
+ Bug reports and feedback collection component for Convex applications with AI analysis, email notifications, and a REST API.
4
4
 
5
5
  ## Features
6
6
 
@@ -12,6 +12,9 @@ Bug reports and feedback collection component for Convex applications with AI an
12
12
  - 👥 **Support Teams** - Route notifications to the right teams based on type/severity
13
13
  - 📸 **Screenshots** - Capture or upload screenshots with reports
14
14
  - 🔄 **Real-time** - Leverages Convex for real-time updates
15
+ - 🎫 **Ticket Numbers** - Human-readable ticket IDs (BUG-2025-0001, FB-2025-0001)
16
+ - 🔑 **API Keys** - Secure API key management for external integrations
17
+ - 🌐 **REST API** - HTTP endpoints for external tools (Codebuff, CLI tools, etc.)
15
18
 
16
19
  ## Installation
17
20
 
@@ -57,7 +60,25 @@ app.use(feedback);
57
60
  export default app;
58
61
  ```
59
62
 
60
- ### 2. Configure Environment Variables
63
+ ### 2. Register HTTP Routes (Optional)
64
+
65
+ To enable the REST API for external integrations:
66
+
67
+ ```typescript
68
+ // convex/http.ts
69
+ import { httpRouter } from "convex/server";
70
+ import { registerFeedbackRoutes } from "@fatagnus/convex-feedback";
71
+
72
+ const http = httpRouter();
73
+
74
+ // Register feedback API routes with optional prefix
75
+ registerFeedbackRoutes(http, { pathPrefix: "/feedback" });
76
+
77
+ // Your other routes...
78
+ export default http;
79
+ ```
80
+
81
+ ### 3. Configure Environment Variables
61
82
 
62
83
  Set these environment variables in your Convex dashboard:
63
84
 
@@ -67,7 +88,7 @@ Set these environment variables in your Convex dashboard:
67
88
  | `RESEND_API_KEY` | No | Resend API key for email notifications |
68
89
  | `RESEND_FROM_EMAIL` | No | From email address (default: `bugs@resend.dev`) |
69
90
 
70
- ### 3. Add React Components
91
+ ### 4. Add React Components
71
92
 
72
93
  Wrap your app with the `BugReportProvider` and add the `BugReportButton`:
73
94
 
@@ -103,7 +124,293 @@ function App() {
103
124
  }
104
125
  ```
105
126
 
106
- ## API Reference
127
+ ---
128
+
129
+ ## HTTP REST API
130
+
131
+ The REST API allows external tools (like Codebuff, CLI tools, CI/CD pipelines) to interact with bug reports and feedback.
132
+
133
+ ### Authentication
134
+
135
+ All endpoints require an API key in the `Authorization` header:
136
+
137
+ ```
138
+ Authorization: Bearer fb_your_api_key_here
139
+ ```
140
+
141
+ ### Base URL
142
+
143
+ Your Convex HTTP endpoint + the path prefix you configured:
144
+
145
+ ```
146
+ https://your-deployment.convex.site/feedback/api/...
147
+ ```
148
+
149
+ ### Ticket Number Format
150
+
151
+ All tickets have human-readable IDs:
152
+
153
+ | Type | Format | Example |
154
+ |------|--------|----------|
155
+ | Bug Report | `BUG-YYYY-NNNN` | `BUG-2025-0001` |
156
+ | Feedback | `FB-YYYY-NNNN` | `FB-2025-0042` |
157
+
158
+ ---
159
+
160
+ ### API Key Management
161
+
162
+ API keys are managed via Convex mutations. The full key is only shown once at creation time.
163
+
164
+ #### Create a Key
165
+
166
+ ```typescript
167
+ const result = await ctx.runMutation(api.feedback.apiKeys.create, {
168
+ name: "My Integration",
169
+ expiresAt: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days (optional)
170
+ });
171
+
172
+ console.log(result.key); // fb_a1b2c3d4e5f6g7h8... (save this!)
173
+ console.log(result.keyPrefix); // fb_a1b2c3d4
174
+ ```
175
+
176
+ #### List Keys
177
+
178
+ ```typescript
179
+ const keys = await ctx.runQuery(api.feedback.apiKeys.list, {});
180
+ // Returns array of { _id, keyPrefix, name, expiresAt, isRevoked, isExpired, createdAt }
181
+ ```
182
+
183
+ #### Revoke a Key
184
+
185
+ ```typescript
186
+ await ctx.runMutation(api.feedback.apiKeys.revoke, {
187
+ keyPrefix: "fb_a1b2c3d4",
188
+ });
189
+ ```
190
+
191
+ #### Rotate a Key
192
+
193
+ ```typescript
194
+ const newKey = await ctx.runMutation(api.feedback.apiKeys.rotate, {
195
+ keyPrefix: "fb_a1b2c3d4",
196
+ name: "Rotated Key", // optional
197
+ expiresAt: Date.now() + 90 * 24 * 60 * 60 * 1000, // optional
198
+ });
199
+ ```
200
+
201
+ ---
202
+
203
+ ### Endpoints
204
+
205
+ #### GET `/api/items`
206
+
207
+ List bug reports and/or feedback items with optional filters.
208
+
209
+ **Query Parameters:**
210
+
211
+ | Parameter | Type | Default | Description |
212
+ |-----------|------|---------|-------------|
213
+ | `itemType` | `bug` \| `feedback` \| `all` | `all` | Filter by item type |
214
+ | `status` | string | - | Filter by status |
215
+ | `severity` | `low` \| `medium` \| `high` \| `critical` | - | Filter bugs by severity |
216
+ | `priority` | `nice_to_have` \| `important` \| `critical` | - | Filter feedback by priority |
217
+ | `type` | `feature_request` \| `change_request` \| `general` | - | Filter feedback by type |
218
+ | `limit` | number | `50` | Max items to return |
219
+ | `includeArchived` | `true` \| `false` | `false` | Include archived items |
220
+
221
+ **Bug Status Values:** `open`, `in-progress`, `resolved`, `closed`
222
+
223
+ **Feedback Status Values:** `open`, `under_review`, `planned`, `in_progress`, `completed`, `declined`
224
+
225
+ **Example:**
226
+
227
+ ```bash
228
+ # List all open bugs
229
+ curl -H "Authorization: Bearer fb_your_key" \
230
+ "https://your-site.convex.site/feedback/api/items?itemType=bug&status=open"
231
+ ```
232
+
233
+ **Response:**
234
+
235
+ ```json
236
+ {
237
+ "bugs": [
238
+ {
239
+ "_id": "abc123",
240
+ "ticketNumber": "BUG-2025-0001",
241
+ "title": "Login button not working",
242
+ "severity": "high",
243
+ "status": "open",
244
+ ...
245
+ }
246
+ ],
247
+ "feedback": [...]
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ #### GET `/api/items/{ticketNumber}`
254
+
255
+ Fetch a single bug report or feedback item by ticket number.
256
+
257
+ **Example:**
258
+
259
+ ```bash
260
+ curl -H "Authorization: Bearer fb_your_key" \
261
+ "https://your-site.convex.site/feedback/api/items/BUG-2025-0001"
262
+ ```
263
+
264
+ **Response:**
265
+
266
+ ```json
267
+ {
268
+ "type": "bug",
269
+ "_id": "abc123",
270
+ "ticketNumber": "BUG-2025-0001",
271
+ "title": "Login button not working",
272
+ "description": "When I click the login button, nothing happens",
273
+ "severity": "high",
274
+ "status": "open",
275
+ "aiSummary": "The login form submit handler is not properly bound",
276
+ "aiRootCauseAnalysis": "React event handler not attached correctly",
277
+ "aiSuggestedFix": "Check that the onClick handler is properly bound",
278
+ ...
279
+ }
280
+ ```
281
+
282
+ ---
283
+
284
+ #### GET `/api/prompt/{ticketNumber}`
285
+
286
+ Fetch an AI-generated prompt for fixing/implementing a ticket. Designed for AI coding assistants like Codebuff.
287
+
288
+ **Query Parameters:**
289
+
290
+ | Parameter | Type | Default | Description |
291
+ |-----------|------|---------|-------------|
292
+ | `template` | `fix` \| `implement` \| `analyze` \| `codebuff` | `fix` (bugs) / `implement` (feedback) | Prompt template |
293
+
294
+ **Templates:**
295
+
296
+ | Template | Use Case |
297
+ |----------|----------|
298
+ | `fix` | Bug fix instructions |
299
+ | `implement` | Feature implementation instructions |
300
+ | `analyze` | Deep analysis prompt |
301
+ | `codebuff` | Codebuff-optimized structured format |
302
+
303
+ **Example:**
304
+
305
+ ```bash
306
+ # Get a Codebuff-optimized prompt
307
+ curl -H "Authorization: Bearer fb_your_key" \
308
+ "https://your-site.convex.site/feedback/api/prompt/BUG-2025-0001?template=codebuff"
309
+ ```
310
+
311
+ **Response:**
312
+
313
+ ```json
314
+ {
315
+ "ticketNumber": "BUG-2025-0001",
316
+ "type": "bug",
317
+ "template": "codebuff",
318
+ "prompt": "# Bug Fix Request: BUG-2025-0001\n\n## Title\nLogin button not working\n\n..."
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ #### PATCH `/api/items/{ticketNumber}/status`
325
+
326
+ Update the status of a bug report or feedback item.
327
+
328
+ **Request Body:**
329
+
330
+ ```json
331
+ { "status": "resolved" }
332
+ ```
333
+
334
+ **Example:**
335
+
336
+ ```bash
337
+ curl -X PATCH \
338
+ -H "Authorization: Bearer fb_your_key" \
339
+ -H "Content-Type: application/json" \
340
+ -d '{"status": "resolved"}' \
341
+ "https://your-site.convex.site/feedback/api/items/BUG-2025-0001/status"
342
+ ```
343
+
344
+ **Response:**
345
+
346
+ ```json
347
+ { "success": true, "ticketNumber": "BUG-2025-0001" }
348
+ ```
349
+
350
+ ---
351
+
352
+ #### PATCH `/api/items/{ticketNumber}/archive`
353
+
354
+ Archive or unarchive a bug report or feedback item.
355
+
356
+ **Request Body:**
357
+
358
+ ```json
359
+ { "archived": true }
360
+ ```
361
+
362
+ **Example:**
363
+
364
+ ```bash
365
+ curl -X PATCH \
366
+ -H "Authorization: Bearer fb_your_key" \
367
+ -H "Content-Type: application/json" \
368
+ -d '{"archived": true}' \
369
+ "https://your-site.convex.site/feedback/api/items/BUG-2025-0001/archive"
370
+ ```
371
+
372
+ **Response:**
373
+
374
+ ```json
375
+ { "success": true, "ticketNumber": "BUG-2025-0001", "isArchived": true }
376
+ ```
377
+
378
+ ---
379
+
380
+ ### Error Responses
381
+
382
+ | Status | Response |
383
+ |--------|----------|
384
+ | 400 | `{ "error": "Bad request", "message": "..." }` |
385
+ | 401 | `{ "error": "Unauthorized", "message": "Invalid or missing API key" }` |
386
+ | 404 | `{ "error": "Not found", "message": "Bug report BUG-2025-0001 not found" }` |
387
+
388
+ ---
389
+
390
+ ### Integration Examples
391
+
392
+ #### Codebuff Integration
393
+
394
+ ```bash
395
+ curl -s -H "Authorization: Bearer fb_your_key" \
396
+ "https://your-site.convex.site/feedback/api/prompt/BUG-2025-0001?template=codebuff" \
397
+ | jq -r '.prompt' \
398
+ | codebuff
399
+ ```
400
+
401
+ #### GitHub Actions
402
+
403
+ ```yaml
404
+ - name: Get open bugs count
405
+ run: |
406
+ curl -s -H "Authorization: Bearer ${{ secrets.FEEDBACK_API_KEY }}" \
407
+ "${{ secrets.CONVEX_SITE_URL }}/feedback/api/items?itemType=bug&status=open" \
408
+ | jq '.bugs | length'
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Convex API Reference
107
414
 
108
415
  ### Bug Reports
109
416
 
@@ -251,6 +558,8 @@ await ctx.runMutation(api.feedback.supportTeams.update, {
251
558
  });
252
559
  ```
253
560
 
561
+ ---
562
+
254
563
  ## React Components
255
564
 
256
565
  ### BugReportProvider
@@ -407,6 +716,8 @@ function MyComponent() {
407
716
  }
408
717
  ```
409
718
 
719
+ ---
720
+
410
721
  ## AI Interview Mode
411
722
 
412
723
  The AI Interview Mode provides a conversational experience to help users articulate their bug reports and feedback more effectively. Instead of filling out a form, users chat with an AI that asks clarifying questions and generates a well-structured report.
@@ -548,6 +859,8 @@ The AI can ask for input in three formats:
548
859
  - Users can always toggle between "AI Interview" and "Quick Form" modes
549
860
  - Screenshots and browser diagnostics are still captured automatically in interview mode
550
861
 
862
+ ---
863
+
551
864
  ## AI Analysis
552
865
 
553
866
  When `OPENROUTER_API_KEY` is configured, the component automatically analyzes submissions:
@@ -569,6 +882,8 @@ When `OPENROUTER_API_KEY` is configured, the component automatically analyzes su
569
882
  - **Estimated Effort** - Low/Medium/High effort estimate
570
883
  - **Suggested Priority** - AI-recommended priority level
571
884
 
885
+ ---
886
+
572
887
  ## Email Notifications
573
888
 
574
889
  When `RESEND_API_KEY` is configured, the component sends email notifications:
@@ -584,6 +899,8 @@ When `RESEND_API_KEY` is configured, the component sends email notifications:
584
899
  - Routed based on feedback type or bug severity
585
900
  - Rich HTML template with all diagnostics
586
901
 
902
+ ---
903
+
587
904
  ## Schema
588
905
 
589
906
  The component creates these tables:
@@ -593,6 +910,31 @@ The component creates these tables:
593
910
  - `supportTeams` - Team configuration for notification routing
594
911
  - `feedbackInputRequests` - Pending user input requests during AI interviews
595
912
  - `interviewSessions` - Interview state and generated reports
913
+ - `ticketCounters` - Sequential counters for ticket numbers
914
+ - `apiKeys` - API keys for REST API authentication
915
+
916
+ ---
917
+
918
+ ## TypeScript Types
919
+
920
+ ```typescript
921
+ import type {
922
+ BugReport,
923
+ Feedback,
924
+ BugSeverity,
925
+ BugStatus,
926
+ FeedbackType,
927
+ FeedbackPriority,
928
+ FeedbackStatus,
929
+ ApiKey,
930
+ ApiKeyCreated,
931
+ PromptTemplate,
932
+ RegisterFeedbackRoutesOptions,
933
+ InterviewContext,
934
+ } from '@fatagnus/convex-feedback';
935
+ ```
936
+
937
+ ---
596
938
 
597
939
  ## License
598
940
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fatagnus/convex-feedback",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Bug reports and feedback collection component for Convex applications with AI analysis and email notifications",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -38,7 +38,8 @@
38
38
  },
39
39
  "./convex.config": "./src/convex/convex.config.ts",
40
40
  "./convex/*": "./src/convex/*",
41
- "./convex/agents/*": "./src/convex/agents/*"
41
+ "./convex/agents/*": "./src/convex/agents/*",
42
+ "./convex/http": "./src/convex/http.ts"
42
43
  },
43
44
  "files": [
44
45
  "dist",
@@ -49,8 +50,12 @@
49
50
  "scripts": {
50
51
  "build": "tsc",
51
52
  "dev": "tsc --watch",
52
- "typecheck": "tsc --noEmit",
53
- "clean": "rm -rf dist"
53
+ "typecheck": "tsc --noEmit --project tsconfig.json",
54
+ "typecheck:convex": "tsc --noEmit --project src/convex/tsconfig.json",
55
+ "postgenerate": "node -e \"const fs=require('fs');const f='src/convex/_generated/api.ts';if(fs.existsSync(f)){let c=fs.readFileSync(f,'utf8');if(!c.includes('@ts-nocheck')){c=c.replace('/* eslint-disable */','/* eslint-disable */\\n// @ts-nocheck - Circular reference errors in generated Convex component types are expected');fs.writeFileSync(f,c)}}\"",
56
+ "clean": "rm -rf dist",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest"
54
59
  },
55
60
  "peerDependencies": {
56
61
  "@ai-sdk/openai-compatible": ">=0.1.0",
@@ -101,6 +106,8 @@
101
106
  "devDependencies": {
102
107
  "@types/node": "^20.0.0",
103
108
  "@types/react": "^18.0.0",
104
- "typescript": "^5.0.0"
109
+ "typescript": "^5.0.0",
110
+ "vitest": "^2.0.0",
111
+ "convex-test": "^0.0.36"
105
112
  }
106
113
  }
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable */
2
+ // @ts-nocheck - Circular reference errors in generated Convex component types are expected
2
3
  /**
3
4
  * Generated `api` utility.
4
5
  *
@@ -591,14 +591,12 @@ export const startBugInterview = action({
591
591
  });
592
592
 
593
593
  // Start the interview
594
+ // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx
594
595
  let result;
595
596
  try {
596
597
  result = await dynamicAgent.generateText(
597
598
  ctx,
598
- {
599
- threadId,
600
- customCtx: { sessionId },
601
- },
599
+ { threadId },
602
600
  { prompt: "Start the bug report interview. Greet the user briefly and ask what bug or issue they encountered." }
603
601
  );
604
602
  } catch (error) {
@@ -722,14 +720,12 @@ export const startFeedbackInterview = action({
722
720
  });
723
721
 
724
722
  // Start the interview
723
+ // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx
725
724
  let result;
726
725
  try {
727
726
  result = await dynamicAgent.generateText(
728
727
  ctx,
729
- {
730
- threadId,
731
- customCtx: { sessionId },
732
- },
728
+ { threadId },
733
729
  { prompt: "Start the feedback interview. Greet the user briefly and ask about their idea or suggestion." }
734
730
  );
735
731
  } catch (error) {
@@ -871,14 +867,12 @@ export const continueInterview = action({
871
867
  });
872
868
 
873
869
  // Continue the agent
870
+ // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx
874
871
  let result;
875
872
  try {
876
873
  result = await dynamicAgent.generateText(
877
874
  ctx,
878
- {
879
- threadId: args.threadId,
880
- customCtx: { sessionId: args.sessionId },
881
- },
875
+ { threadId: args.threadId },
882
876
  {
883
877
  prompt: `The user responded: ${args.response}
884
878
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Unit tests for API Key utilities
3
+ *
4
+ * Note: Testing Convex component packages with convex-test requires special setup.
5
+ * These tests verify the key format and structure expectations.
6
+ * Full integration tests should be run in a real Convex deployment.
7
+ */
8
+ import { describe, it, expect } from "vitest";
9
+
10
+ describe("API Key Format", () => {
11
+ describe("Key Structure", () => {
12
+ it("key format should be fb_ followed by 32 hex chars", () => {
13
+ // Example of expected format
14
+ const validKeyPattern = /^fb_[a-f0-9]{32}$/;
15
+ const exampleKey = "fb_0123456789abcdef0123456789abcdef";
16
+
17
+ expect(exampleKey).toMatch(validKeyPattern);
18
+ expect(exampleKey.length).toBe(35); // "fb_" (3) + 32 hex chars
19
+ });
20
+
21
+ it("key prefix should be fb_ followed by 8 hex chars", () => {
22
+ const validPrefixPattern = /^fb_[a-f0-9]{8}$/;
23
+ const examplePrefix = "fb_01234567";
24
+
25
+ expect(examplePrefix).toMatch(validPrefixPattern);
26
+ expect(examplePrefix.length).toBe(11); // "fb_" (3) + 8 hex chars
27
+ });
28
+
29
+ it("key prefix should be the first 11 chars of the full key", () => {
30
+ const fullKey = "fb_0123456789abcdef0123456789abcdef";
31
+ const expectedPrefix = fullKey.slice(0, 11);
32
+
33
+ expect(expectedPrefix).toBe("fb_01234567");
34
+ });
35
+ });
36
+
37
+ describe("Key Hashing", () => {
38
+ it("SHA-256 hash should produce 64 hex chars", async () => {
39
+ const key = "fb_0123456789abcdef0123456789abcdef";
40
+
41
+ const encoder = new TextEncoder();
42
+ const data = encoder.encode(key);
43
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
44
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
45
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
46
+
47
+ expect(hashHex.length).toBe(64);
48
+ expect(hashHex).toMatch(/^[a-f0-9]{64}$/);
49
+ });
50
+
51
+ it("same key should always produce same hash", async () => {
52
+ const key = "fb_testkey123456789abcdef12345678";
53
+
54
+ const hash1 = await hashKey(key);
55
+ const hash2 = await hashKey(key);
56
+
57
+ expect(hash1).toBe(hash2);
58
+ });
59
+
60
+ it("different keys should produce different hashes", async () => {
61
+ const key1 = "fb_testkey123456789abcdef12345678";
62
+ const key2 = "fb_testkey123456789abcdef12345679";
63
+
64
+ const hash1 = await hashKey(key1);
65
+ const hash2 = await hashKey(key2);
66
+
67
+ expect(hash1).not.toBe(hash2);
68
+ });
69
+ });
70
+ });
71
+
72
+ // Helper function matching the implementation in apiKeys.ts
73
+ async function hashKey(key: string): Promise<string> {
74
+ const encoder = new TextEncoder();
75
+ const data = encoder.encode(key);
76
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
77
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
78
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
79
+ }