@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.
@@ -0,0 +1,630 @@
1
+ import { httpRouter, HttpRouter } from "convex/server";
2
+ import { httpAction } from "./_generated/server";
3
+ import { internal } from "./_generated/api";
4
+ import { generateBugPrompt, generateFeedbackPrompt, PromptTemplate } from "./prompts";
5
+ import type { RegisterFeedbackRoutesOptions } from "../types";
6
+
7
+ export type { RegisterFeedbackRoutesOptions };
8
+
9
+ /**
10
+ * Validate API key from Authorization header
11
+ */
12
+ async function validateApiKey(
13
+ ctx: { runQuery: (fn: unknown, args: unknown) => Promise<unknown>; runMutation?: (fn: unknown, args: unknown) => Promise<unknown> },
14
+ authHeader: string | null
15
+ ): Promise<boolean> {
16
+ if (!authHeader) {
17
+ return false;
18
+ }
19
+
20
+ // Expect: "Bearer fb_xxxxx"
21
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
22
+ if (!match) {
23
+ return false;
24
+ }
25
+
26
+ const key = match[1];
27
+ return (await ctx.runQuery(internal.apiKeys.validateKey, { key })) as boolean;
28
+ }
29
+
30
+ /**
31
+ * Parse template from query string
32
+ */
33
+ function parseTemplate(url: URL): PromptTemplate {
34
+ const template = url.searchParams.get("template");
35
+ if (template === "fix" || template === "implement" || template === "analyze" || template === "codebuff") {
36
+ return template;
37
+ }
38
+ // Return undefined to let the caller decide the default
39
+ return "fix";
40
+ }
41
+
42
+ /**
43
+ * Register all feedback HTTP routes on the provided router.
44
+ *
45
+ * This allows consumer apps to integrate the feedback API into their own HTTP router.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // In your app's convex/http.ts:
50
+ * import { httpRouter } from "convex/server";
51
+ * import { registerFeedbackRoutes } from "@convex-dev/feedback/convex";
52
+ *
53
+ * const http = httpRouter();
54
+ *
55
+ * // Register feedback routes with optional prefix
56
+ * registerFeedbackRoutes(http, { pathPrefix: "/feedback" });
57
+ *
58
+ * // Your other routes...
59
+ * http.route({ path: "/api/health", method: "GET", handler: ... });
60
+ *
61
+ * export default http;
62
+ * ```
63
+ *
64
+ * Registered routes:
65
+ * - GET {prefix}/api/prompt/{ticketNumber} - Fetch AI prompt for a ticket
66
+ * - GET {prefix}/api/items - List bug reports and feedback
67
+ * - GET {prefix}/api/items/{ticketNumber} - Get a single item
68
+ * - PATCH {prefix}/api/items/{ticketNumber}/status - Update item status
69
+ * - PATCH {prefix}/api/items/{ticketNumber}/archive - Archive/unarchive item
70
+ *
71
+ * @param router - The HttpRouter to register routes on
72
+ * @param options - Optional configuration
73
+ */
74
+ export function registerFeedbackRoutes(
75
+ router: HttpRouter,
76
+ options: RegisterFeedbackRoutesOptions = {}
77
+ ): void {
78
+ const prefix = options.pathPrefix ?? "";
79
+
80
+ // GET /api/prompt/{ticketNumber}
81
+ router.route({
82
+ path: `${prefix}/api/prompt/{ticketNumber}`,
83
+ method: "GET",
84
+ handler: httpAction(async (ctx, request) => {
85
+ // Validate API key
86
+ const authHeader = request.headers.get("Authorization");
87
+ const isValid = await validateApiKey(ctx, authHeader);
88
+
89
+ if (!isValid) {
90
+ return new Response(
91
+ JSON.stringify({ error: "Unauthorized", message: "Invalid or missing API key" }),
92
+ {
93
+ status: 401,
94
+ headers: { "Content-Type": "application/json" },
95
+ }
96
+ );
97
+ }
98
+
99
+ // Extract ticket number from URL
100
+ const url = new URL(request.url);
101
+ const pathParts = url.pathname.split("/");
102
+ const ticketNumber = pathParts[pathParts.length - 1]?.toUpperCase();
103
+
104
+ if (!ticketNumber) {
105
+ return new Response(
106
+ JSON.stringify({ error: "Bad request", message: "Missing ticket number" }),
107
+ {
108
+ status: 400,
109
+ headers: { "Content-Type": "application/json" },
110
+ }
111
+ );
112
+ }
113
+
114
+ const template = parseTemplate(url);
115
+ const isBug = ticketNumber.startsWith("BUG-");
116
+ const isFeedback = ticketNumber.startsWith("FB-");
117
+
118
+ if (!isBug && !isFeedback) {
119
+ return new Response(
120
+ JSON.stringify({ error: "Bad request", message: "Invalid ticket number format" }),
121
+ {
122
+ status: 400,
123
+ headers: { "Content-Type": "application/json" },
124
+ }
125
+ );
126
+ }
127
+
128
+ if (isBug) {
129
+ const bug = await ctx.runQuery(internal.prompts.getBugByTicketNumber, { ticketNumber });
130
+
131
+ if (!bug) {
132
+ return new Response(
133
+ JSON.stringify({ error: "Not found", message: `Bug report ${ticketNumber} not found` }),
134
+ {
135
+ status: 404,
136
+ headers: { "Content-Type": "application/json" },
137
+ }
138
+ );
139
+ }
140
+
141
+ const prompt = generateBugPrompt(
142
+ {
143
+ ticketNumber: bug.ticketNumber,
144
+ title: bug.title,
145
+ description: bug.description,
146
+ severity: bug.severity,
147
+ url: bug.url,
148
+ route: bug.route,
149
+ browserInfo: bug.browserInfo,
150
+ consoleErrors: bug.consoleErrors,
151
+ viewportWidth: bug.viewportWidth,
152
+ viewportHeight: bug.viewportHeight,
153
+ aiSummary: bug.aiSummary,
154
+ aiRootCauseAnalysis: bug.aiRootCauseAnalysis,
155
+ aiReproductionSteps: bug.aiReproductionSteps,
156
+ aiSuggestedFix: bug.aiSuggestedFix,
157
+ aiEstimatedEffort: bug.aiEstimatedEffort,
158
+ createdAt: bug.createdAt,
159
+ },
160
+ template
161
+ );
162
+
163
+ return new Response(
164
+ JSON.stringify({
165
+ ticketNumber: bug.ticketNumber,
166
+ type: "bug",
167
+ template,
168
+ prompt,
169
+ }),
170
+ {
171
+ status: 200,
172
+ headers: { "Content-Type": "application/json" },
173
+ }
174
+ );
175
+ }
176
+
177
+ // Feedback
178
+ const feedback = await ctx.runQuery(internal.prompts.getFeedbackByTicketNumber, { ticketNumber });
179
+
180
+ if (!feedback) {
181
+ return new Response(
182
+ JSON.stringify({ error: "Not found", message: `Feedback ${ticketNumber} not found` }),
183
+ {
184
+ status: 404,
185
+ headers: { "Content-Type": "application/json" },
186
+ }
187
+ );
188
+ }
189
+
190
+ // For feedback, default to "implement" instead of "fix"
191
+ const feedbackTemplate = template === "fix" && !url.searchParams.has("template") ? "implement" : template;
192
+
193
+ const prompt = generateFeedbackPrompt(
194
+ {
195
+ ticketNumber: feedback.ticketNumber,
196
+ type: feedback.type,
197
+ title: feedback.title,
198
+ description: feedback.description,
199
+ priority: feedback.priority,
200
+ url: feedback.url,
201
+ route: feedback.route,
202
+ aiSummary: feedback.aiSummary,
203
+ aiImpactAnalysis: feedback.aiImpactAnalysis,
204
+ aiActionItems: feedback.aiActionItems,
205
+ aiEstimatedEffort: feedback.aiEstimatedEffort,
206
+ createdAt: feedback.createdAt,
207
+ },
208
+ feedbackTemplate
209
+ );
210
+
211
+ return new Response(
212
+ JSON.stringify({
213
+ ticketNumber: feedback.ticketNumber,
214
+ type: "feedback",
215
+ template: feedbackTemplate,
216
+ prompt,
217
+ }),
218
+ {
219
+ status: 200,
220
+ headers: { "Content-Type": "application/json" },
221
+ }
222
+ );
223
+ }),
224
+ });
225
+
226
+ // GET /api/items/{ticketNumber}
227
+ /**
228
+ * GET /api/items/{ticketNumber}
229
+ *
230
+ * Fetch a single bug report or feedback item by ticket number.
231
+ *
232
+ * Path params:
233
+ * - ticketNumber: e.g., "BUG-2025-0001" or "FB-2025-0001"
234
+ *
235
+ * Headers:
236
+ * - Authorization: Bearer <api-key>
237
+ *
238
+ * Returns:
239
+ * - 200: Full item data
240
+ * - 400: { error: "Bad request" }
241
+ * - 401: { error: "Unauthorized" }
242
+ * - 404: { error: "Not found" }
243
+ */
244
+ router.route({
245
+ path: `${prefix}/api/items/{ticketNumber}`,
246
+ method: "GET",
247
+ handler: httpAction(async (ctx, request) => {
248
+ const authHeader = request.headers.get("Authorization");
249
+ const isValid = await validateApiKey(ctx, authHeader);
250
+
251
+ if (!isValid) {
252
+ return new Response(
253
+ JSON.stringify({ error: "Unauthorized", message: "Invalid or missing API key" }),
254
+ { status: 401, headers: { "Content-Type": "application/json" } }
255
+ );
256
+ }
257
+
258
+ // Extract ticket number from URL
259
+ const url = new URL(request.url);
260
+ const pathParts = url.pathname.split("/");
261
+ const ticketNumber = pathParts[pathParts.length - 1]?.toUpperCase();
262
+
263
+ if (!ticketNumber) {
264
+ return new Response(
265
+ JSON.stringify({ error: "Bad request", message: "Missing ticket number" }),
266
+ { status: 400, headers: { "Content-Type": "application/json" } }
267
+ );
268
+ }
269
+
270
+ const isBug = ticketNumber.startsWith("BUG-");
271
+ const isFeedback = ticketNumber.startsWith("FB-");
272
+
273
+ if (!isBug && !isFeedback) {
274
+ return new Response(
275
+ JSON.stringify({ error: "Bad request", message: "Invalid ticket number format. Expected BUG-YYYY-NNNN or FB-YYYY-NNNN" }),
276
+ { status: 400, headers: { "Content-Type": "application/json" } }
277
+ );
278
+ }
279
+
280
+ if (isBug) {
281
+ const bug = await ctx.runQuery(internal.prompts.getBugByTicketNumber, { ticketNumber });
282
+
283
+ if (!bug) {
284
+ return new Response(
285
+ JSON.stringify({ error: "Not found", message: `Bug report ${ticketNumber} not found` }),
286
+ { status: 404, headers: { "Content-Type": "application/json" } }
287
+ );
288
+ }
289
+
290
+ return new Response(
291
+ JSON.stringify({ type: "bug", ...bug }),
292
+ { status: 200, headers: { "Content-Type": "application/json" } }
293
+ );
294
+ }
295
+
296
+ // Feedback
297
+ const feedback = await ctx.runQuery(internal.prompts.getFeedbackByTicketNumber, { ticketNumber });
298
+
299
+ if (!feedback) {
300
+ return new Response(
301
+ JSON.stringify({ error: "Not found", message: `Feedback ${ticketNumber} not found` }),
302
+ { status: 404, headers: { "Content-Type": "application/json" } }
303
+ );
304
+ }
305
+
306
+ return new Response(
307
+ JSON.stringify({ type: "feedback", ...feedback }),
308
+ { status: 200, headers: { "Content-Type": "application/json" } }
309
+ );
310
+ }),
311
+ });
312
+
313
+ // GET /api/items
314
+ /**
315
+ * GET /api/items
316
+ *
317
+ * List bug reports and/or feedback items with optional filters.
318
+ *
319
+ * Query params:
320
+ * - itemType: "bug" | "feedback" | "all" (default: "all")
321
+ * - status: filter by status (bug: open|in-progress|resolved|closed, feedback: open|under_review|planned|in_progress|completed|declined)
322
+ * - severity: filter bugs by severity (low|medium|high|critical)
323
+ * - priority: filter feedback by priority (nice_to_have|important|critical)
324
+ * - type: filter feedback by type (feature_request|change_request|general)
325
+ * - limit: max items to return (default: 50)
326
+ * - includeArchived: include archived items (default: false)
327
+ *
328
+ * Headers:
329
+ * - Authorization: Bearer <api-key>
330
+ *
331
+ * Returns:
332
+ * - 200: { bugs: [...], feedback: [...] }
333
+ * - 401: { error: "Unauthorized" }
334
+ */
335
+ router.route({
336
+ path: `${prefix}/api/items`,
337
+ method: "GET",
338
+ handler: httpAction(async (ctx, request) => {
339
+ const authHeader = request.headers.get("Authorization");
340
+ const isValid = await validateApiKey(ctx, authHeader);
341
+
342
+ if (!isValid) {
343
+ return new Response(
344
+ JSON.stringify({ error: "Unauthorized", message: "Invalid or missing API key" }),
345
+ { status: 401, headers: { "Content-Type": "application/json" } }
346
+ );
347
+ }
348
+
349
+ const url = new URL(request.url);
350
+ const itemType = url.searchParams.get("itemType") ?? "all";
351
+ const status = url.searchParams.get("status");
352
+ const severity = url.searchParams.get("severity");
353
+ const priority = url.searchParams.get("priority");
354
+ const feedbackType = url.searchParams.get("type");
355
+ const limitParam = url.searchParams.get("limit");
356
+ const limit = limitParam ? parseInt(limitParam, 10) : 50;
357
+ const includeArchived = url.searchParams.get("includeArchived") === "true";
358
+
359
+ const result: { bugs?: unknown[]; feedback?: unknown[] } = {};
360
+
361
+ // Fetch bugs if requested
362
+ if (itemType === "bug" || itemType === "all") {
363
+ const bugArgs: Record<string, unknown> = { limit, includeArchived };
364
+ if (status && ["open", "in-progress", "resolved", "closed"].includes(status)) {
365
+ bugArgs.status = status;
366
+ }
367
+ if (severity && ["low", "medium", "high", "critical"].includes(severity)) {
368
+ bugArgs.severity = severity;
369
+ }
370
+ result.bugs = (await ctx.runQuery(internal.bugReports.listInternal, bugArgs)) as unknown[];
371
+ }
372
+
373
+ // Fetch feedback if requested
374
+ if (itemType === "feedback" || itemType === "all") {
375
+ const feedbackArgs: Record<string, unknown> = { limit, includeArchived };
376
+ if (status && ["open", "under_review", "planned", "in_progress", "completed", "declined"].includes(status)) {
377
+ feedbackArgs.status = status;
378
+ }
379
+ if (priority && ["nice_to_have", "important", "critical"].includes(priority)) {
380
+ feedbackArgs.priority = priority;
381
+ }
382
+ if (feedbackType && ["feature_request", "change_request", "general"].includes(feedbackType)) {
383
+ feedbackArgs.type = feedbackType;
384
+ }
385
+ result.feedback = (await ctx.runQuery(internal.feedback.listInternal, feedbackArgs)) as unknown[];
386
+ }
387
+
388
+ return new Response(JSON.stringify(result), {
389
+ status: 200,
390
+ headers: { "Content-Type": "application/json" },
391
+ });
392
+ }),
393
+ });
394
+
395
+ // PATCH /api/items/{ticketNumber}/status
396
+ /**
397
+ * PATCH /api/items/{ticketNumber}/status
398
+ *
399
+ * Update the status of a bug report or feedback item.
400
+ *
401
+ * Path params:
402
+ * - ticketNumber: e.g., "BUG-2025-0001" or "FB-2025-0001"
403
+ *
404
+ * Body (JSON):
405
+ * - status: new status value
406
+ * - For bugs: "open" | "in-progress" | "resolved" | "closed"
407
+ * - For feedback: "open" | "under_review" | "planned" | "in_progress" | "completed" | "declined"
408
+ *
409
+ * Headers:
410
+ * - Authorization: Bearer <api-key>
411
+ * - Content-Type: application/json
412
+ *
413
+ * Returns:
414
+ * - 200: { success: true, ticketNumber: "..." }
415
+ * - 400: { error: "Bad request" }
416
+ * - 401: { error: "Unauthorized" }
417
+ * - 404: { error: "Not found" }
418
+ */
419
+ router.route({
420
+ path: `${prefix}/api/items/{ticketNumber}/status`,
421
+ method: "PATCH",
422
+ handler: httpAction(async (ctx, request) => {
423
+ const authHeader = request.headers.get("Authorization");
424
+ const isValid = await validateApiKey(ctx, authHeader);
425
+
426
+ if (!isValid) {
427
+ return new Response(
428
+ JSON.stringify({ error: "Unauthorized", message: "Invalid or missing API key" }),
429
+ { status: 401, headers: { "Content-Type": "application/json" } }
430
+ );
431
+ }
432
+
433
+ // Extract ticket number from URL
434
+ const url = new URL(request.url);
435
+ const pathParts = url.pathname.split("/");
436
+ // Path is /api/items/{ticketNumber}/status, so ticketNumber is second to last
437
+ const ticketNumber = pathParts[pathParts.length - 2]?.toUpperCase();
438
+
439
+ if (!ticketNumber) {
440
+ return new Response(
441
+ JSON.stringify({ error: "Bad request", message: "Missing ticket number" }),
442
+ { status: 400, headers: { "Content-Type": "application/json" } }
443
+ );
444
+ }
445
+
446
+ const isBug = ticketNumber.startsWith("BUG-");
447
+ const isFeedback = ticketNumber.startsWith("FB-");
448
+
449
+ if (!isBug && !isFeedback) {
450
+ return new Response(
451
+ JSON.stringify({ error: "Bad request", message: "Invalid ticket number format. Expected BUG-YYYY-NNNN or FB-YYYY-NNNN" }),
452
+ { status: 400, headers: { "Content-Type": "application/json" } }
453
+ );
454
+ }
455
+
456
+ // Parse request body
457
+ let body: { status?: string };
458
+ try {
459
+ body = await request.json();
460
+ } catch {
461
+ return new Response(
462
+ JSON.stringify({ error: "Bad request", message: "Invalid JSON body" }),
463
+ { status: 400, headers: { "Content-Type": "application/json" } }
464
+ );
465
+ }
466
+
467
+ if (!body.status) {
468
+ return new Response(
469
+ JSON.stringify({ error: "Bad request", message: "Missing status in request body" }),
470
+ { status: 400, headers: { "Content-Type": "application/json" } }
471
+ );
472
+ }
473
+
474
+ // Validate status based on type
475
+ const bugStatuses = ["open", "in-progress", "resolved", "closed"];
476
+ const feedbackStatuses = ["open", "under_review", "planned", "in_progress", "completed", "declined"];
477
+
478
+ if (isBug && !bugStatuses.includes(body.status)) {
479
+ return new Response(
480
+ JSON.stringify({ error: "Bad request", message: `Invalid bug status. Expected one of: ${bugStatuses.join(", ")}` }),
481
+ { status: 400, headers: { "Content-Type": "application/json" } }
482
+ );
483
+ }
484
+
485
+ if (isFeedback && !feedbackStatuses.includes(body.status)) {
486
+ return new Response(
487
+ JSON.stringify({ error: "Bad request", message: `Invalid feedback status. Expected one of: ${feedbackStatuses.join(", ")}` }),
488
+ { status: 400, headers: { "Content-Type": "application/json" } }
489
+ );
490
+ }
491
+
492
+ // Update the status
493
+ let result;
494
+ if (isBug) {
495
+ result = await ctx.runMutation(internal.bugReports.updateStatusByTicketNumber, {
496
+ ticketNumber,
497
+ status: body.status as "open" | "in-progress" | "resolved" | "closed",
498
+ });
499
+ } else {
500
+ result = await ctx.runMutation(internal.feedback.updateStatusByTicketNumber, {
501
+ ticketNumber,
502
+ status: body.status as "open" | "under_review" | "planned" | "in_progress" | "completed" | "declined",
503
+ });
504
+ }
505
+
506
+ if (!result) {
507
+ return new Response(
508
+ JSON.stringify({ error: "Not found", message: `Item ${ticketNumber} not found` }),
509
+ { status: 404, headers: { "Content-Type": "application/json" } }
510
+ );
511
+ }
512
+
513
+ return new Response(JSON.stringify(result), {
514
+ status: 200,
515
+ headers: { "Content-Type": "application/json" },
516
+ });
517
+ }),
518
+ });
519
+
520
+ // PATCH /api/items/{ticketNumber}/archive
521
+ /**
522
+ * PATCH /api/items/{ticketNumber}/archive
523
+ *
524
+ * Archive or unarchive a bug report or feedback item.
525
+ *
526
+ * Path params:
527
+ * - ticketNumber: e.g., "BUG-2025-0001" or "FB-2025-0001"
528
+ *
529
+ * Body (JSON):
530
+ * - archived: boolean (true to archive, false to unarchive)
531
+ *
532
+ * Headers:
533
+ * - Authorization: Bearer <api-key>
534
+ * - Content-Type: application/json
535
+ *
536
+ * Returns:
537
+ * - 200: { success: true, ticketNumber: "...", isArchived: true/false }
538
+ * - 400: { error: "Bad request" }
539
+ * - 401: { error: "Unauthorized" }
540
+ * - 404: { error: "Not found" }
541
+ */
542
+ router.route({
543
+ path: `${prefix}/api/items/{ticketNumber}/archive`,
544
+ method: "PATCH",
545
+ handler: httpAction(async (ctx, request) => {
546
+ const authHeader = request.headers.get("Authorization");
547
+ const isValid = await validateApiKey(ctx, authHeader);
548
+
549
+ if (!isValid) {
550
+ return new Response(
551
+ JSON.stringify({ error: "Unauthorized", message: "Invalid or missing API key" }),
552
+ { status: 401, headers: { "Content-Type": "application/json" } }
553
+ );
554
+ }
555
+
556
+ // Extract ticket number from URL
557
+ const url = new URL(request.url);
558
+ const pathParts = url.pathname.split("/");
559
+ // Path is /api/items/{ticketNumber}/archive, so ticketNumber is second to last
560
+ const ticketNumber = pathParts[pathParts.length - 2]?.toUpperCase();
561
+
562
+ if (!ticketNumber) {
563
+ return new Response(
564
+ JSON.stringify({ error: "Bad request", message: "Missing ticket number" }),
565
+ { status: 400, headers: { "Content-Type": "application/json" } }
566
+ );
567
+ }
568
+
569
+ const isBug = ticketNumber.startsWith("BUG-");
570
+ const isFeedback = ticketNumber.startsWith("FB-");
571
+
572
+ if (!isBug && !isFeedback) {
573
+ return new Response(
574
+ JSON.stringify({ error: "Bad request", message: "Invalid ticket number format. Expected BUG-YYYY-NNNN or FB-YYYY-NNNN" }),
575
+ { status: 400, headers: { "Content-Type": "application/json" } }
576
+ );
577
+ }
578
+
579
+ // Parse request body
580
+ let body: { archived?: boolean };
581
+ try {
582
+ body = await request.json();
583
+ } catch {
584
+ return new Response(
585
+ JSON.stringify({ error: "Bad request", message: "Invalid JSON body" }),
586
+ { status: 400, headers: { "Content-Type": "application/json" } }
587
+ );
588
+ }
589
+
590
+ if (typeof body.archived !== "boolean") {
591
+ return new Response(
592
+ JSON.stringify({ error: "Bad request", message: "Missing or invalid 'archived' boolean in request body" }),
593
+ { status: 400, headers: { "Content-Type": "application/json" } }
594
+ );
595
+ }
596
+
597
+ // Update the archive status
598
+ let result;
599
+ if (isBug) {
600
+ result = await ctx.runMutation(internal.bugReports.setArchivedByTicketNumber, {
601
+ ticketNumber,
602
+ isArchived: body.archived,
603
+ });
604
+ } else {
605
+ result = await ctx.runMutation(internal.feedback.setArchivedByTicketNumber, {
606
+ ticketNumber,
607
+ isArchived: body.archived,
608
+ });
609
+ }
610
+
611
+ if (!result) {
612
+ return new Response(
613
+ JSON.stringify({ error: "Not found", message: `Item ${ticketNumber} not found` }),
614
+ { status: 404, headers: { "Content-Type": "application/json" } }
615
+ );
616
+ }
617
+
618
+ return new Response(JSON.stringify(result), {
619
+ status: 200,
620
+ headers: { "Content-Type": "application/json" },
621
+ });
622
+ }),
623
+ });
624
+ }
625
+
626
+ // Create a standalone router for backwards compatibility / standalone usage
627
+ const http = httpRouter();
628
+ registerFeedbackRoutes(http);
629
+
630
+ export default http;
@@ -10,6 +10,11 @@ export * as feedback from './feedback';
10
10
  export * as supportTeams from './supportTeams';
11
11
  export * as inputRequests from './inputRequests';
12
12
  export * as agents from './agents';
13
+ export * as apiKeys from './apiKeys';
14
+ export * as prompts from './prompts';
15
+
16
+ // Export HTTP route registration helper
17
+ export { registerFeedbackRoutes, type RegisterFeedbackRoutesOptions } from './http';
13
18
 
14
19
  // Re-export schema types and validators
15
20
  export {
@@ -20,6 +25,7 @@ export {
20
25
  feedbackStatusValidator,
21
26
  reporterTypeValidator,
22
27
  effortValidator,
28
+ counterTypeValidator,
23
29
  type BugSeverity,
24
30
  type BugStatus,
25
31
  type FeedbackType,
@@ -27,4 +33,9 @@ export {
27
33
  type FeedbackStatus,
28
34
  type ReporterType,
29
35
  type Effort,
36
+ type CounterType,
30
37
  } from './schema';
38
+
39
+ // Re-export prompt template type
40
+ export type { PromptTemplate } from './prompts';
41
+ export { promptTemplateValidator } from './prompts';