@open-skills-hub/api 1.0.0

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 (112) hide show
  1. package/dist/controllers/audit.d.ts +33 -0
  2. package/dist/controllers/audit.d.ts.map +1 -0
  3. package/dist/controllers/audit.js +122 -0
  4. package/dist/controllers/audit.js.map +1 -0
  5. package/dist/controllers/cache.d.ts +42 -0
  6. package/dist/controllers/cache.d.ts.map +1 -0
  7. package/dist/controllers/cache.js +247 -0
  8. package/dist/controllers/cache.js.map +1 -0
  9. package/dist/controllers/feedback.d.ts +44 -0
  10. package/dist/controllers/feedback.d.ts.map +1 -0
  11. package/dist/controllers/feedback.js +216 -0
  12. package/dist/controllers/feedback.js.map +1 -0
  13. package/dist/controllers/index.d.ts +9 -0
  14. package/dist/controllers/index.d.ts.map +1 -0
  15. package/dist/controllers/index.js +9 -0
  16. package/dist/controllers/index.js.map +1 -0
  17. package/dist/controllers/skills.d.ts +66 -0
  18. package/dist/controllers/skills.d.ts.map +1 -0
  19. package/dist/controllers/skills.js +355 -0
  20. package/dist/controllers/skills.js.map +1 -0
  21. package/dist/controllers/versions.d.ts +43 -0
  22. package/dist/controllers/versions.d.ts.map +1 -0
  23. package/dist/controllers/versions.js +298 -0
  24. package/dist/controllers/versions.js.map +1 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +78 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/middleware/auth.d.ts +34 -0
  30. package/dist/middleware/auth.d.ts.map +1 -0
  31. package/dist/middleware/auth.js +148 -0
  32. package/dist/middleware/auth.js.map +1 -0
  33. package/dist/middleware/error.d.ts +26 -0
  34. package/dist/middleware/error.d.ts.map +1 -0
  35. package/dist/middleware/error.js +102 -0
  36. package/dist/middleware/error.js.map +1 -0
  37. package/dist/middleware/index.d.ts +8 -0
  38. package/dist/middleware/index.d.ts.map +1 -0
  39. package/dist/middleware/index.js +8 -0
  40. package/dist/middleware/index.js.map +1 -0
  41. package/dist/middleware/logger.d.ts +19 -0
  42. package/dist/middleware/logger.d.ts.map +1 -0
  43. package/dist/middleware/logger.js +54 -0
  44. package/dist/middleware/logger.js.map +1 -0
  45. package/dist/middleware/validation.d.ts +671 -0
  46. package/dist/middleware/validation.d.ts.map +1 -0
  47. package/dist/middleware/validation.js +225 -0
  48. package/dist/middleware/validation.js.map +1 -0
  49. package/dist/routes/audit.d.ts +6 -0
  50. package/dist/routes/audit.d.ts.map +1 -0
  51. package/dist/routes/audit.js +54 -0
  52. package/dist/routes/audit.js.map +1 -0
  53. package/dist/routes/cache.d.ts +6 -0
  54. package/dist/routes/cache.d.ts.map +1 -0
  55. package/dist/routes/cache.js +70 -0
  56. package/dist/routes/cache.js.map +1 -0
  57. package/dist/routes/feedback.d.ts +6 -0
  58. package/dist/routes/feedback.d.ts.map +1 -0
  59. package/dist/routes/feedback.js +68 -0
  60. package/dist/routes/feedback.js.map +1 -0
  61. package/dist/routes/health.d.ts +6 -0
  62. package/dist/routes/health.d.ts.map +1 -0
  63. package/dist/routes/health.js +122 -0
  64. package/dist/routes/health.js.map +1 -0
  65. package/dist/routes/index.d.ts +12 -0
  66. package/dist/routes/index.d.ts.map +1 -0
  67. package/dist/routes/index.js +12 -0
  68. package/dist/routes/index.js.map +1 -0
  69. package/dist/routes/scan.d.ts +8 -0
  70. package/dist/routes/scan.d.ts.map +1 -0
  71. package/dist/routes/scan.js +315 -0
  72. package/dist/routes/scan.js.map +1 -0
  73. package/dist/routes/search.d.ts +6 -0
  74. package/dist/routes/search.d.ts.map +1 -0
  75. package/dist/routes/search.js +44 -0
  76. package/dist/routes/search.js.map +1 -0
  77. package/dist/routes/skills.d.ts +6 -0
  78. package/dist/routes/skills.d.ts.map +1 -0
  79. package/dist/routes/skills.js +74 -0
  80. package/dist/routes/skills.js.map +1 -0
  81. package/dist/routes/versions.d.ts +6 -0
  82. package/dist/routes/versions.d.ts.map +1 -0
  83. package/dist/routes/versions.js +66 -0
  84. package/dist/routes/versions.js.map +1 -0
  85. package/dist/server.d.ts +26 -0
  86. package/dist/server.d.ts.map +1 -0
  87. package/dist/server.js +166 -0
  88. package/dist/server.js.map +1 -0
  89. package/package.json +42 -0
  90. package/src/controllers/audit.ts +175 -0
  91. package/src/controllers/cache.ts +344 -0
  92. package/src/controllers/feedback.ts +309 -0
  93. package/src/controllers/index.ts +9 -0
  94. package/src/controllers/skills.ts +489 -0
  95. package/src/controllers/versions.ts +427 -0
  96. package/src/index.ts +87 -0
  97. package/src/middleware/auth.ts +219 -0
  98. package/src/middleware/error.ts +180 -0
  99. package/src/middleware/index.ts +8 -0
  100. package/src/middleware/logger.ts +71 -0
  101. package/src/middleware/validation.ts +270 -0
  102. package/src/routes/audit.ts +74 -0
  103. package/src/routes/cache.ts +93 -0
  104. package/src/routes/feedback.ts +93 -0
  105. package/src/routes/health.ts +151 -0
  106. package/src/routes/index.ts +12 -0
  107. package/src/routes/scan.ts +428 -0
  108. package/src/routes/search.ts +51 -0
  109. package/src/routes/skills.ts +102 -0
  110. package/src/routes/versions.ts +91 -0
  111. package/src/server.ts +205 -0
  112. package/tsconfig.json +13 -0
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Open Skills Hub - Cache Controller
3
+ */
4
+
5
+ import type { FastifyRequest, FastifyReply } from 'fastify';
6
+ import {
7
+ getStorage,
8
+ getConfig,
9
+ generateUUID,
10
+ now,
11
+ calculateExpiry,
12
+ buildSkillFullName,
13
+ parseSkillFullName,
14
+ AppError,
15
+ ErrorCodes,
16
+ } from '@open-skills-hub/core';
17
+ import type { CacheMetadata, AuditLog } from '@open-skills-hub/core';
18
+ import { sendSuccess } from '../middleware/error.js';
19
+ import { logger } from '../middleware/logger.js';
20
+ import type {
21
+ SkillNameParam,
22
+ SkillVersionParam,
23
+ } from '../middleware/validation.js';
24
+
25
+ /**
26
+ * Get all cached skills
27
+ */
28
+ export async function getCachedSkills(
29
+ request: FastifyRequest,
30
+ reply: FastifyReply
31
+ ): Promise<void> {
32
+ const storage = await getStorage();
33
+ const cacheEntries = await storage.getAllCacheMetadata();
34
+
35
+ // Group by skill name
36
+ const bySkill: Record<string, {
37
+ skillName: string;
38
+ versions: {
39
+ version: string;
40
+ cachedAt: string;
41
+ expiresAt?: string;
42
+ size?: number;
43
+ hitCount: number;
44
+ lastHitAt?: string;
45
+ }[];
46
+ totalHits: number;
47
+ totalSize: number;
48
+ }> = {};
49
+
50
+ for (const entry of cacheEntries) {
51
+ if (!bySkill[entry.skillName]) {
52
+ bySkill[entry.skillName] = {
53
+ skillName: entry.skillName,
54
+ versions: [],
55
+ totalHits: 0,
56
+ totalSize: 0,
57
+ };
58
+ }
59
+
60
+ bySkill[entry.skillName]!.versions.push({
61
+ version: entry.version,
62
+ cachedAt: entry.cachedAt,
63
+ expiresAt: entry.expiresAt,
64
+ size: entry.size,
65
+ hitCount: entry.hitCount,
66
+ lastHitAt: entry.lastHitAt,
67
+ });
68
+
69
+ bySkill[entry.skillName]!.totalHits += entry.hitCount;
70
+ bySkill[entry.skillName]!.totalSize += entry.size ?? 0;
71
+ }
72
+
73
+ const skills = Object.values(bySkill).sort((a, b) => b.totalHits - a.totalHits);
74
+
75
+ sendSuccess(request, reply, {
76
+ count: cacheEntries.length,
77
+ skills,
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Get cache info for a specific skill
83
+ */
84
+ export async function getSkillCacheInfo(
85
+ request: FastifyRequest<{ Params: SkillNameParam }>,
86
+ reply: FastifyReply
87
+ ): Promise<void> {
88
+ const storage = await getStorage();
89
+ const { name } = request.params;
90
+
91
+ // Parse name
92
+ const { scope, name: skillName } = parseSkillFullName(name);
93
+ const fullName = buildSkillFullName(skillName, scope);
94
+
95
+ const allCache = await storage.getAllCacheMetadata();
96
+ const skillCache = allCache.filter(c => c.skillName === fullName);
97
+
98
+ if (skillCache.length === 0) {
99
+ throw new AppError(
100
+ ErrorCodes.CACHE_MISS,
101
+ `No cache entries found for skill '${fullName}'`,
102
+ 404
103
+ );
104
+ }
105
+
106
+ const totalHits = skillCache.reduce((sum, c) => sum + c.hitCount, 0);
107
+ const totalSize = skillCache.reduce((sum, c) => sum + (c.size ?? 0), 0);
108
+
109
+ sendSuccess(request, reply, {
110
+ skillName: fullName,
111
+ versions: skillCache.map(c => ({
112
+ version: c.version,
113
+ cachedAt: c.cachedAt,
114
+ expiresAt: c.expiresAt,
115
+ size: c.size,
116
+ hitCount: c.hitCount,
117
+ lastHitAt: c.lastHitAt,
118
+ integrityHash: c.integrityHash,
119
+ })),
120
+ totalVersions: skillCache.length,
121
+ totalHits,
122
+ totalSize,
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Get cache info for a specific version
128
+ */
129
+ export async function getVersionCacheInfo(
130
+ request: FastifyRequest<{ Params: SkillVersionParam }>,
131
+ reply: FastifyReply
132
+ ): Promise<void> {
133
+ const storage = await getStorage();
134
+ const { name, version } = request.params;
135
+
136
+ // Parse name
137
+ const { scope, name: skillName } = parseSkillFullName(name);
138
+ const fullName = buildSkillFullName(skillName, scope);
139
+
140
+ const cacheEntry = await storage.getCacheMetadata(fullName, version);
141
+ if (!cacheEntry) {
142
+ throw new AppError(
143
+ ErrorCodes.CACHE_MISS,
144
+ `No cache entry found for '${fullName}@${version}'`,
145
+ 404
146
+ );
147
+ }
148
+
149
+ sendSuccess(request, reply, cacheEntry);
150
+ }
151
+
152
+ /**
153
+ * Invalidate cache for a skill
154
+ */
155
+ export async function invalidateSkillCache(
156
+ request: FastifyRequest<{ Params: SkillNameParam }>,
157
+ reply: FastifyReply
158
+ ): Promise<void> {
159
+ const storage = await getStorage();
160
+ const { name } = request.params;
161
+ const userId = request.user?.id;
162
+
163
+ // Parse name
164
+ const { scope, name: skillName } = parseSkillFullName(name);
165
+ const fullName = buildSkillFullName(skillName, scope);
166
+
167
+ const deleted = await storage.deleteCacheMetadata(fullName);
168
+
169
+ // Create audit log
170
+ const auditLog: AuditLog = {
171
+ id: generateUUID(),
172
+ timestamp: now(),
173
+ eventType: 'cache.invalidated',
174
+ actor: {
175
+ type: request.user?.type ?? 'user',
176
+ id: userId,
177
+ username: request.user?.username,
178
+ ip: request.ip,
179
+ userAgent: request.headers['user-agent'],
180
+ },
181
+ resource: {
182
+ type: 'skill',
183
+ name: fullName,
184
+ },
185
+ action: 'invalidate_cache',
186
+ result: deleted ? 'success' : 'failure',
187
+ };
188
+ await storage.createAuditLog(auditLog);
189
+
190
+ logger.info('Cache invalidated', { skillName: fullName, deleted });
191
+
192
+ sendSuccess(request, reply, { invalidated: deleted });
193
+ }
194
+
195
+ /**
196
+ * Invalidate cache for a specific version
197
+ */
198
+ export async function invalidateVersionCache(
199
+ request: FastifyRequest<{ Params: SkillVersionParam }>,
200
+ reply: FastifyReply
201
+ ): Promise<void> {
202
+ const storage = await getStorage();
203
+ const { name, version } = request.params;
204
+ const userId = request.user?.id;
205
+
206
+ // Parse name
207
+ const { scope, name: skillName } = parseSkillFullName(name);
208
+ const fullName = buildSkillFullName(skillName, scope);
209
+
210
+ const deleted = await storage.deleteCacheMetadata(fullName, version);
211
+
212
+ // Create audit log
213
+ const auditLog: AuditLog = {
214
+ id: generateUUID(),
215
+ timestamp: now(),
216
+ eventType: 'cache.invalidated',
217
+ actor: {
218
+ type: request.user?.type ?? 'user',
219
+ id: userId,
220
+ username: request.user?.username,
221
+ ip: request.ip,
222
+ userAgent: request.headers['user-agent'],
223
+ },
224
+ resource: {
225
+ type: 'version',
226
+ name: fullName,
227
+ version,
228
+ },
229
+ action: 'invalidate_cache',
230
+ result: deleted ? 'success' : 'failure',
231
+ };
232
+ await storage.createAuditLog(auditLog);
233
+
234
+ logger.info('Version cache invalidated', { skillName: fullName, version, deleted });
235
+
236
+ sendSuccess(request, reply, { invalidated: deleted });
237
+ }
238
+
239
+ /**
240
+ * Clean expired cache entries
241
+ */
242
+ export async function cleanExpiredCache(
243
+ request: FastifyRequest,
244
+ reply: FastifyReply
245
+ ): Promise<void> {
246
+ const storage = await getStorage();
247
+ const userId = request.user?.id;
248
+
249
+ const expiredEntries = await storage.getExpiredCacheEntries();
250
+ let cleaned = 0;
251
+
252
+ for (const entry of expiredEntries) {
253
+ const deleted = await storage.deleteCacheMetadata(entry.skillName, entry.version);
254
+ if (deleted) cleaned++;
255
+ }
256
+
257
+ // Create audit log
258
+ const auditLog: AuditLog = {
259
+ id: generateUUID(),
260
+ timestamp: now(),
261
+ eventType: 'cache.invalidated',
262
+ actor: {
263
+ type: request.user?.type ?? 'system',
264
+ id: userId,
265
+ username: request.user?.username,
266
+ ip: request.ip,
267
+ userAgent: request.headers['user-agent'],
268
+ },
269
+ resource: {
270
+ type: 'config',
271
+ name: 'cache',
272
+ },
273
+ action: 'clean_expired',
274
+ result: 'success',
275
+ details: {
276
+ expired: expiredEntries.length,
277
+ cleaned,
278
+ },
279
+ };
280
+ await storage.createAuditLog(auditLog);
281
+
282
+ logger.info('Expired cache cleaned', { expired: expiredEntries.length, cleaned });
283
+
284
+ sendSuccess(request, reply, {
285
+ expired: expiredEntries.length,
286
+ cleaned,
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Get cache statistics
292
+ */
293
+ export async function getCacheStats(
294
+ request: FastifyRequest,
295
+ reply: FastifyReply
296
+ ): Promise<void> {
297
+ const storage = await getStorage();
298
+ const config = getConfig().get();
299
+
300
+ const cacheEntries = await storage.getAllCacheMetadata();
301
+ const expiredEntries = await storage.getExpiredCacheEntries();
302
+
303
+ const totalSize = cacheEntries.reduce((sum, c) => sum + (c.size ?? 0), 0);
304
+ const totalHits = cacheEntries.reduce((sum, c) => sum + c.hitCount, 0);
305
+ const uniqueSkills = new Set(cacheEntries.map(c => c.skillName)).size;
306
+
307
+ // Most accessed
308
+ const mostAccessed = [...cacheEntries]
309
+ .sort((a, b) => b.hitCount - a.hitCount)
310
+ .slice(0, 10)
311
+ .map(c => ({
312
+ skillName: c.skillName,
313
+ version: c.version,
314
+ hitCount: c.hitCount,
315
+ }));
316
+
317
+ // Recently cached
318
+ const recentlyCached = [...cacheEntries]
319
+ .sort((a, b) => b.cachedAt.localeCompare(a.cachedAt))
320
+ .slice(0, 10)
321
+ .map(c => ({
322
+ skillName: c.skillName,
323
+ version: c.version,
324
+ cachedAt: c.cachedAt,
325
+ }));
326
+
327
+ sendSuccess(request, reply, {
328
+ totalEntries: cacheEntries.length,
329
+ uniqueSkills,
330
+ totalSize,
331
+ totalHits,
332
+ expiredCount: expiredEntries.length,
333
+ config: {
334
+ ttl: config.cache.ttl,
335
+ maxSize: config.cache.maxSize,
336
+ strategy: config.cache.strategy,
337
+ },
338
+ utilization: config.cache.maxSize > 0
339
+ ? Math.round((totalSize / config.cache.maxSize) * 100)
340
+ : 0,
341
+ mostAccessed,
342
+ recentlyCached,
343
+ });
344
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Open Skills Hub - Feedback Controller
3
+ */
4
+
5
+ import type { FastifyRequest, FastifyReply } from 'fastify';
6
+ import {
7
+ getStorage,
8
+ generateUUID,
9
+ now,
10
+ hashIP,
11
+ buildSkillFullName,
12
+ parseSkillFullName,
13
+ AppError,
14
+ ErrorCodes,
15
+ } from '@open-skills-hub/core';
16
+ import type { Feedback, AuditLog } from '@open-skills-hub/core';
17
+ import { sendSuccess } from '../middleware/error.js';
18
+ import { logger } from '../middleware/logger.js';
19
+ import type {
20
+ SkillNameParam,
21
+ IdParam,
22
+ SubmitFeedbackBody,
23
+ PaginationQuery,
24
+ } from '../middleware/validation.js';
25
+
26
+ /**
27
+ * Submit feedback for a skill
28
+ */
29
+ export async function submitFeedback(
30
+ request: FastifyRequest<{ Body: SubmitFeedbackBody }>,
31
+ reply: FastifyReply
32
+ ): Promise<void> {
33
+ const storage = await getStorage();
34
+ const body = request.body;
35
+
36
+ // Parse name
37
+ const { scope, name: skillName } = parseSkillFullName(body.skillName);
38
+ const fullName = buildSkillFullName(skillName, scope);
39
+
40
+ const skill = await storage.getSkillByName(fullName);
41
+ if (!skill) {
42
+ throw new AppError(
43
+ ErrorCodes.SKILL_NOT_FOUND,
44
+ `Skill '${fullName}' not found`,
45
+ 404
46
+ );
47
+ }
48
+
49
+ // Verify version exists
50
+ const version = await storage.getVersion(skill.id, body.skillVersion);
51
+ if (!version) {
52
+ throw new AppError(
53
+ ErrorCodes.VERSION_NOT_FOUND,
54
+ `Version '${body.skillVersion}' not found for skill '${fullName}'`,
55
+ 404
56
+ );
57
+ }
58
+
59
+ const feedbackId = generateUUID();
60
+ const timestamp = now();
61
+
62
+ const feedback: Feedback = {
63
+ id: feedbackId,
64
+ skillId: skill.id,
65
+ skillVersion: body.skillVersion,
66
+ feedbackType: body.feedbackType,
67
+ rating: body.rating,
68
+ comment: body.comment,
69
+ context: body.context ?? {},
70
+ status: 'pending',
71
+ sourceIpHash: hashIP(request.ip),
72
+ createdAt: timestamp,
73
+ };
74
+
75
+ await storage.createFeedback(feedback);
76
+
77
+ // Update skill rating if this is a rating feedback
78
+ if (body.rating) {
79
+ const feedbacks = await storage.getFeedbacks(skill.id, { limit: 1000 });
80
+ const ratings = feedbacks.items.map(f => f.rating).filter((r): r is number => r !== undefined);
81
+ const averageRating = ratings.length > 0
82
+ ? ratings.reduce((sum, r) => sum + r, 0) / ratings.length
83
+ : 0;
84
+
85
+ await storage.updateSkill(skill.id, {
86
+ rating: {
87
+ average: Math.round(averageRating * 10) / 10,
88
+ count: ratings.length,
89
+ },
90
+ });
91
+ }
92
+
93
+ // Create audit log
94
+ const auditLog: AuditLog = {
95
+ id: generateUUID(),
96
+ timestamp,
97
+ eventType: 'feedback.submitted',
98
+ actor: {
99
+ type: request.user?.type ?? 'user',
100
+ id: request.user?.id,
101
+ username: request.user?.username,
102
+ ip: request.ip,
103
+ userAgent: request.headers['user-agent'],
104
+ },
105
+ resource: {
106
+ type: 'feedback',
107
+ id: feedbackId,
108
+ name: fullName,
109
+ version: body.skillVersion,
110
+ },
111
+ action: 'create',
112
+ result: 'success',
113
+ details: {
114
+ feedbackType: body.feedbackType,
115
+ rating: body.rating,
116
+ },
117
+ };
118
+ await storage.createAuditLog(auditLog);
119
+
120
+ logger.info('Feedback submitted', {
121
+ feedbackId,
122
+ skillName: fullName,
123
+ feedbackType: body.feedbackType,
124
+ rating: body.rating,
125
+ });
126
+
127
+ sendSuccess(request, reply, {
128
+ id: feedbackId,
129
+ status: 'pending',
130
+ createdAt: timestamp,
131
+ }, 201);
132
+ }
133
+
134
+ /**
135
+ * Get feedbacks for a skill
136
+ */
137
+ export async function getSkillFeedbacks(
138
+ request: FastifyRequest<{
139
+ Params: SkillNameParam;
140
+ Querystring: PaginationQuery & { feedbackType?: string; status?: string }
141
+ }>,
142
+ reply: FastifyReply
143
+ ): Promise<void> {
144
+ const storage = await getStorage();
145
+ const { name } = request.params;
146
+ const { cursor, limit, feedbackType, status } = request.query;
147
+
148
+ // Parse name
149
+ const { scope, name: skillName } = parseSkillFullName(name);
150
+ const fullName = buildSkillFullName(skillName, scope);
151
+
152
+ const skill = await storage.getSkillByName(fullName);
153
+ if (!skill) {
154
+ throw new AppError(
155
+ ErrorCodes.SKILL_NOT_FOUND,
156
+ `Skill '${fullName}' not found`,
157
+ 404
158
+ );
159
+ }
160
+
161
+ const result = await storage.getFeedbacks(skill.id, {
162
+ cursor,
163
+ limit,
164
+ feedbackType,
165
+ status,
166
+ });
167
+
168
+ // Remove sensitive data
169
+ const feedbacks = result.items.map(f => ({
170
+ id: f.id,
171
+ skillVersion: f.skillVersion,
172
+ feedbackType: f.feedbackType,
173
+ rating: f.rating,
174
+ comment: f.comment,
175
+ context: f.context,
176
+ status: f.status,
177
+ createdAt: f.createdAt,
178
+ }));
179
+
180
+ sendSuccess(request, reply, feedbacks, 200, result.pagination);
181
+ }
182
+
183
+ /**
184
+ * Get feedback by ID
185
+ */
186
+ export async function getFeedbackById(
187
+ request: FastifyRequest<{ Params: IdParam }>,
188
+ reply: FastifyReply
189
+ ): Promise<void> {
190
+ const storage = await getStorage();
191
+ const { id } = request.params;
192
+
193
+ const feedback = await storage.getFeedbackById(id);
194
+ if (!feedback) {
195
+ throw new AppError(
196
+ ErrorCodes.SKILL_NOT_FOUND, // Using generic not found
197
+ `Feedback '${id}' not found`,
198
+ 404
199
+ );
200
+ }
201
+
202
+ sendSuccess(request, reply, {
203
+ id: feedback.id,
204
+ skillId: feedback.skillId,
205
+ skillVersion: feedback.skillVersion,
206
+ feedbackType: feedback.feedbackType,
207
+ rating: feedback.rating,
208
+ comment: feedback.comment,
209
+ context: feedback.context,
210
+ status: feedback.status,
211
+ createdAt: feedback.createdAt,
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Review feedback (admin)
217
+ */
218
+ export async function reviewFeedback(
219
+ request: FastifyRequest<{
220
+ Params: IdParam;
221
+ Body: { status: 'reviewed' | 'accepted' | 'rejected'; notes?: string }
222
+ }>,
223
+ reply: FastifyReply
224
+ ): Promise<void> {
225
+ const storage = await getStorage();
226
+ const { id } = request.params;
227
+ const { status, notes } = request.body;
228
+ const userId = request.user?.id;
229
+
230
+ const feedback = await storage.getFeedbackById(id);
231
+ if (!feedback) {
232
+ throw new AppError(
233
+ ErrorCodes.SKILL_NOT_FOUND,
234
+ `Feedback '${id}' not found`,
235
+ 404
236
+ );
237
+ }
238
+
239
+ const updated = await storage.updateFeedback(id, {
240
+ status,
241
+ reviewerNotes: notes,
242
+ reviewedAt: now(),
243
+ reviewedBy: userId,
244
+ });
245
+
246
+ logger.info('Feedback reviewed', {
247
+ feedbackId: id,
248
+ status,
249
+ reviewedBy: userId,
250
+ });
251
+
252
+ sendSuccess(request, reply, {
253
+ id: updated?.id,
254
+ status: updated?.status,
255
+ reviewerNotes: updated?.reviewerNotes,
256
+ reviewedAt: updated?.reviewedAt,
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Get feedback statistics for a skill
262
+ */
263
+ export async function getFeedbackStats(
264
+ request: FastifyRequest<{ Params: SkillNameParam }>,
265
+ reply: FastifyReply
266
+ ): Promise<void> {
267
+ const storage = await getStorage();
268
+ const { name } = request.params;
269
+
270
+ // Parse name
271
+ const { scope, name: skillName } = parseSkillFullName(name);
272
+ const fullName = buildSkillFullName(skillName, scope);
273
+
274
+ const skill = await storage.getSkillByName(fullName);
275
+ if (!skill) {
276
+ throw new AppError(
277
+ ErrorCodes.SKILL_NOT_FOUND,
278
+ `Skill '${fullName}' not found`,
279
+ 404
280
+ );
281
+ }
282
+
283
+ // Get all feedbacks
284
+ const result = await storage.getFeedbacks(skill.id, { limit: 1000 });
285
+ const feedbacks = result.items;
286
+
287
+ // Calculate stats
288
+ const stats = {
289
+ total: feedbacks.length,
290
+ byType: {
291
+ success: feedbacks.filter(f => f.feedbackType === 'success').length,
292
+ failure: feedbacks.filter(f => f.feedbackType === 'failure').length,
293
+ suggestion: feedbacks.filter(f => f.feedbackType === 'suggestion').length,
294
+ bug: feedbacks.filter(f => f.feedbackType === 'bug').length,
295
+ },
296
+ byStatus: {
297
+ pending: feedbacks.filter(f => f.status === 'pending').length,
298
+ reviewed: feedbacks.filter(f => f.status === 'reviewed').length,
299
+ accepted: feedbacks.filter(f => f.status === 'accepted').length,
300
+ rejected: feedbacks.filter(f => f.status === 'rejected').length,
301
+ },
302
+ rating: skill.rating,
303
+ successRate: feedbacks.length > 0
304
+ ? feedbacks.filter(f => f.feedbackType === 'success').length / feedbacks.length
305
+ : 0,
306
+ };
307
+
308
+ sendSuccess(request, reply, stats);
309
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Open Skills Hub - Controllers Exports
3
+ */
4
+
5
+ export * from './skills.js';
6
+ export * from './versions.js';
7
+ export * from './feedback.js';
8
+ export * from './audit.js';
9
+ export * from './cache.js';