@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,428 @@
1
+ /**
2
+ * Open Skills Hub - Scan Routes
3
+ *
4
+ * Security scanning endpoints using SkillsGuard
5
+ */
6
+
7
+ import type { FastifyInstance, FastifyPluginOptions, FastifyRequest, FastifyReply } from 'fastify';
8
+ import { z } from 'zod';
9
+ import {
10
+ getStorage,
11
+ getConfig,
12
+ initScanner,
13
+ getScanner,
14
+ generateUUID,
15
+ now,
16
+ buildSkillFullName,
17
+ parseSkillFullName,
18
+ AppError,
19
+ ErrorCodes,
20
+ } from '@open-skills-hub/core';
21
+ import type { AuditLog } from '@open-skills-hub/core';
22
+ import { sendSuccess } from '../middleware/error.js';
23
+ import { logger } from '../middleware/logger.js';
24
+ import { optionalAuth, requireAuth } from '../middleware/auth.js';
25
+ import {
26
+ validateBody,
27
+ validateParams,
28
+ validateQuery,
29
+ SkillNameParamSchema,
30
+ ScanContentBodySchema,
31
+ PaginationQuerySchema,
32
+ IdParamSchema,
33
+ } from '../middleware/validation.js';
34
+
35
+ // Initialize scanner on first use
36
+ let scannerInitialized = false;
37
+
38
+ function ensureScannerInitialized() {
39
+ if (!scannerInitialized) {
40
+ const config = getConfig().get();
41
+ initScanner({
42
+ enabled: config.scanner.enabled,
43
+ timeout: config.scanner.timeout,
44
+ maxFileSize: config.scanner.maxFileSize,
45
+ rulesPath: config.scanner.rulesPath,
46
+ });
47
+ scannerInitialized = true;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Scan content
53
+ */
54
+ async function scanContent(
55
+ request: FastifyRequest<{ Body: z.infer<typeof ScanContentBodySchema> }>,
56
+ reply: FastifyReply
57
+ ): Promise<void> {
58
+ ensureScannerInitialized();
59
+
60
+ const storage = await getStorage();
61
+ const scanner = getScanner();
62
+ const { content } = request.body;
63
+ const userId = request.user?.id;
64
+
65
+ // Run the scan
66
+ const result = await scanner.scan(content);
67
+
68
+ // Save scan record and issues to storage
69
+ await storage.createScanRecord({
70
+ ...result.record,
71
+ userId,
72
+ inputType: 'api',
73
+ });
74
+
75
+ if (result.issues.length > 0) {
76
+ await storage.createScanIssues(result.issues);
77
+ }
78
+
79
+ // Create audit log
80
+ const auditLog: AuditLog = {
81
+ id: generateUUID(),
82
+ timestamp: now(),
83
+ eventType: 'scan.completed',
84
+ actor: {
85
+ type: request.user?.type ?? 'user',
86
+ id: userId,
87
+ username: request.user?.username,
88
+ ip: request.ip,
89
+ userAgent: request.headers['user-agent'],
90
+ },
91
+ resource: {
92
+ type: 'skill',
93
+ id: result.record.id,
94
+ },
95
+ action: 'scan',
96
+ result: 'success',
97
+ details: {
98
+ score: result.summary.score,
99
+ level: result.summary.level,
100
+ issueCount: result.summary.issueCount,
101
+ duration: result.record.duration,
102
+ },
103
+ };
104
+ await storage.createAuditLog(auditLog);
105
+
106
+ // Log high-risk scans
107
+ if (result.summary.level === 'high') {
108
+ const highRiskLog: AuditLog = {
109
+ id: generateUUID(),
110
+ timestamp: now(),
111
+ eventType: 'scan.high_risk',
112
+ actor: {
113
+ type: request.user?.type ?? 'user',
114
+ id: userId,
115
+ username: request.user?.username,
116
+ ip: request.ip,
117
+ },
118
+ resource: {
119
+ type: 'skill',
120
+ id: result.record.id,
121
+ },
122
+ action: 'scan',
123
+ result: 'success',
124
+ details: {
125
+ score: result.summary.score,
126
+ highSeverityIssues: result.summary.issueCount.high,
127
+ riskCategories: result.summary.riskCategories,
128
+ },
129
+ };
130
+ await storage.createAuditLog(highRiskLog);
131
+ }
132
+
133
+ logger.info('Scan completed', {
134
+ recordId: result.record.id,
135
+ score: result.summary.score,
136
+ level: result.summary.level,
137
+ issueCount: result.summary.issueCount,
138
+ });
139
+
140
+ sendSuccess(request, reply, {
141
+ id: result.record.id,
142
+ score: result.summary.score,
143
+ level: result.summary.level,
144
+ issueCount: result.summary.issueCount,
145
+ issues: result.issues.map(i => ({
146
+ id: i.id,
147
+ ruleId: i.ruleId,
148
+ ruleName: i.ruleName,
149
+ category: i.ruleCategory,
150
+ severity: i.severity,
151
+ line: i.line,
152
+ column: i.column,
153
+ content: i.content,
154
+ message: i.message,
155
+ suggestion: i.suggestion,
156
+ })),
157
+ recommendations: result.summary.recommendations,
158
+ duration: result.record.duration,
159
+ scannerVersion: result.record.scannerVersion,
160
+ rulesApplied: result.record.rulesApplied,
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Quick check content (high-severity only)
166
+ */
167
+ async function quickCheck(
168
+ request: FastifyRequest<{ Body: z.infer<typeof ScanContentBodySchema> }>,
169
+ reply: FastifyReply
170
+ ): Promise<void> {
171
+ ensureScannerInitialized();
172
+
173
+ const scanner = getScanner();
174
+ const { content } = request.body;
175
+
176
+ const result = await scanner.quickCheck(content);
177
+
178
+ sendSuccess(request, reply, result);
179
+ }
180
+
181
+ /**
182
+ * Get scan records for a skill
183
+ */
184
+ async function getSkillScans(
185
+ request: FastifyRequest<{ Params: z.infer<typeof SkillNameParamSchema>; Querystring: z.infer<typeof PaginationQuerySchema> }>,
186
+ reply: FastifyReply
187
+ ): Promise<void> {
188
+ const storage = await getStorage();
189
+ const { name } = request.params;
190
+ const { cursor, limit } = request.query;
191
+
192
+ // Parse name
193
+ const { scope, name: skillName } = parseSkillFullName(name);
194
+ const fullName = buildSkillFullName(skillName, scope);
195
+
196
+ const skill = await storage.getSkillByName(fullName);
197
+ if (!skill) {
198
+ throw new AppError(
199
+ ErrorCodes.SKILL_NOT_FOUND,
200
+ `Skill '${fullName}' not found`,
201
+ 404
202
+ );
203
+ }
204
+
205
+ const result = await storage.getScanRecords(skill.id, { cursor, limit });
206
+
207
+ sendSuccess(request, reply, result.items, 200, result.pagination);
208
+ }
209
+
210
+ /**
211
+ * Get scan record by ID
212
+ */
213
+ async function getScanRecord(
214
+ request: FastifyRequest<{ Params: z.infer<typeof IdParamSchema> }>,
215
+ reply: FastifyReply
216
+ ): Promise<void> {
217
+ const storage = await getStorage();
218
+ const { id } = request.params;
219
+
220
+ const record = await storage.getScanRecordById(id);
221
+ if (!record) {
222
+ throw new AppError(
223
+ ErrorCodes.SKILL_NOT_FOUND,
224
+ `Scan record '${id}' not found`,
225
+ 404
226
+ );
227
+ }
228
+
229
+ // Get issues if scan is completed
230
+ let issues: unknown[] = [];
231
+ if (record.status === 'completed') {
232
+ issues = await storage.getScanIssues(id);
233
+ }
234
+
235
+ sendSuccess(request, reply, {
236
+ ...record,
237
+ issues,
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Get scan issues for a record
243
+ */
244
+ async function getScanIssues(
245
+ request: FastifyRequest<{ Params: z.infer<typeof IdParamSchema> }>,
246
+ reply: FastifyReply
247
+ ): Promise<void> {
248
+ const storage = await getStorage();
249
+ const { id } = request.params;
250
+
251
+ const record = await storage.getScanRecordById(id);
252
+ if (!record) {
253
+ throw new AppError(
254
+ ErrorCodes.SKILL_NOT_FOUND,
255
+ `Scan record '${id}' not found`,
256
+ 404
257
+ );
258
+ }
259
+
260
+ const issues = await storage.getScanIssues(id);
261
+
262
+ sendSuccess(request, reply, issues);
263
+ }
264
+
265
+ /**
266
+ * Acknowledge an issue
267
+ */
268
+ async function acknowledgeIssue(
269
+ request: FastifyRequest<{ Params: z.infer<typeof IdParamSchema>; Body: { reason?: string } }>,
270
+ reply: FastifyReply
271
+ ): Promise<void> {
272
+ const storage = await getStorage();
273
+ const { id } = request.params;
274
+ const { reason } = request.body;
275
+ const userId = request.user?.id ?? 'unknown';
276
+
277
+ const issue = await storage.acknowledgeIssue(id, userId, reason);
278
+ if (!issue) {
279
+ throw new AppError(
280
+ ErrorCodes.SKILL_NOT_FOUND,
281
+ `Issue '${id}' not found`,
282
+ 404
283
+ );
284
+ }
285
+
286
+ logger.info('Issue acknowledged', { issueId: id, userId, reason });
287
+
288
+ sendSuccess(request, reply, issue);
289
+ }
290
+
291
+ /**
292
+ * Get scanner rules
293
+ */
294
+ async function getScannerRules(
295
+ request: FastifyRequest,
296
+ reply: FastifyReply
297
+ ): Promise<void> {
298
+ ensureScannerInitialized();
299
+
300
+ const scanner = getScanner();
301
+ const rules = scanner.getEnabledRules();
302
+
303
+ sendSuccess(request, reply, {
304
+ version: scanner.getVersion(),
305
+ rulesCount: rules.length,
306
+ rules: rules.map(r => ({
307
+ id: r.id,
308
+ name: r.name,
309
+ description: r.description,
310
+ category: r.category,
311
+ severity: r.severity,
312
+ enabled: r.enabled,
313
+ isBuiltin: r.isBuiltin,
314
+ })),
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Get scanner statistics
320
+ */
321
+ async function getScannerStats(
322
+ request: FastifyRequest,
323
+ reply: FastifyReply
324
+ ): Promise<void> {
325
+ ensureScannerInitialized();
326
+
327
+ const scanner = getScanner();
328
+ const rules = scanner.getEnabledRules();
329
+
330
+ // Count rules by category and severity
331
+ const byCategory: Record<string, number> = {};
332
+ const bySeverity: Record<string, number> = {};
333
+
334
+ for (const rule of rules) {
335
+ byCategory[rule.category] = (byCategory[rule.category] || 0) + 1;
336
+ bySeverity[rule.severity] = (bySeverity[rule.severity] || 0) + 1;
337
+ }
338
+
339
+ sendSuccess(request, reply, {
340
+ version: scanner.getVersion(),
341
+ totalRules: rules.length,
342
+ enabledRules: rules.filter(r => r.enabled).length,
343
+ builtinRules: rules.filter(r => r.isBuiltin).length,
344
+ customRules: rules.filter(r => !r.isBuiltin).length,
345
+ byCategory,
346
+ bySeverity,
347
+ });
348
+ }
349
+
350
+ export async function scanRoutes(
351
+ server: FastifyInstance,
352
+ _opts: FastifyPluginOptions
353
+ ): Promise<void> {
354
+ // Scan content
355
+ // POST /v1/scan
356
+ server.post('/', {
357
+ preHandler: [
358
+ optionalAuth,
359
+ validateBody(ScanContentBodySchema),
360
+ ],
361
+ handler: scanContent,
362
+ });
363
+
364
+ // Quick check (high-severity only)
365
+ // POST /v1/scan/quick
366
+ server.post('/quick', {
367
+ preHandler: [
368
+ optionalAuth,
369
+ validateBody(ScanContentBodySchema),
370
+ ],
371
+ handler: quickCheck,
372
+ });
373
+
374
+ // Get scanner rules
375
+ // GET /v1/scan/rules
376
+ server.get('/rules', {
377
+ preHandler: [optionalAuth],
378
+ handler: getScannerRules,
379
+ });
380
+
381
+ // Get scanner statistics
382
+ // GET /v1/scan/stats
383
+ server.get('/stats', {
384
+ preHandler: [optionalAuth],
385
+ handler: getScannerStats,
386
+ });
387
+
388
+ // Get scan records for a skill
389
+ // GET /v1/scan/skill/:name
390
+ server.get('/skill/:name', {
391
+ preHandler: [
392
+ optionalAuth,
393
+ validateParams(SkillNameParamSchema),
394
+ validateQuery(PaginationQuerySchema),
395
+ ],
396
+ handler: getSkillScans,
397
+ });
398
+
399
+ // Get scan record by ID
400
+ // GET /v1/scan/:id
401
+ server.get('/:id', {
402
+ preHandler: [
403
+ optionalAuth,
404
+ validateParams(IdParamSchema),
405
+ ],
406
+ handler: getScanRecord,
407
+ });
408
+
409
+ // Get issues for a scan record
410
+ // GET /v1/scan/:id/issues
411
+ server.get('/:id/issues', {
412
+ preHandler: [
413
+ optionalAuth,
414
+ validateParams(IdParamSchema),
415
+ ],
416
+ handler: getScanIssues,
417
+ });
418
+
419
+ // Acknowledge an issue
420
+ // POST /v1/scan/issues/:id/acknowledge
421
+ server.post('/issues/:id/acknowledge', {
422
+ preHandler: [
423
+ requireAuth,
424
+ validateParams(IdParamSchema),
425
+ ],
426
+ handler: acknowledgeIssue,
427
+ });
428
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Open Skills Hub - Search Routes
3
+ */
4
+
5
+ import type { FastifyInstance, FastifyPluginOptions, FastifyRequest, FastifyReply } from 'fastify';
6
+ import { searchSkills, type SearchQuery } from '../controllers/skills.js';
7
+ import { optionalAuth } from '../middleware/auth.js';
8
+ import { validateQuery, SearchQuerySchema } from '../middleware/validation.js';
9
+
10
+ export async function searchRoutes(
11
+ server: FastifyInstance,
12
+ _opts: FastifyPluginOptions
13
+ ): Promise<void> {
14
+ // Full-text search
15
+ // GET /v1/search?q=...&category=...&sort=...
16
+ server.get('/', {
17
+ preHandler: [
18
+ optionalAuth,
19
+ validateQuery(SearchQuerySchema),
20
+ ],
21
+ handler: searchSkills as (request: FastifyRequest, reply: FastifyReply) => Promise<void>,
22
+ });
23
+
24
+ // Search by category (convenience endpoint)
25
+ // GET /v1/search/category/:category
26
+ server.get('/category/:category', {
27
+ preHandler: [
28
+ optionalAuth,
29
+ ],
30
+ handler: async (request, reply) => {
31
+ const { category } = request.params as { category: string };
32
+ // Forward to main search with category filter
33
+ (request.query as SearchQuery).category = category;
34
+ return (searchSkills as (request: FastifyRequest, reply: FastifyReply) => Promise<void>)(request, reply);
35
+ },
36
+ });
37
+
38
+ // Search by author (convenience endpoint)
39
+ // GET /v1/search/author/:author
40
+ server.get('/author/:author', {
41
+ preHandler: [
42
+ optionalAuth,
43
+ ],
44
+ handler: async (request, reply) => {
45
+ const { author } = request.params as { author: string };
46
+ // Forward to main search with author filter
47
+ (request.query as SearchQuery).author = author;
48
+ return (searchSkills as (request: FastifyRequest, reply: FastifyReply) => Promise<void>)(request, reply);
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Open Skills Hub - Skills Routes
3
+ */
4
+
5
+ import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
6
+ import {
7
+ searchSkills,
8
+ getSkillByName,
9
+ getSkillContent,
10
+ createSkill,
11
+ updateSkill,
12
+ deleteSkill,
13
+ getDerivedSkills,
14
+ } from '../controllers/skills.js';
15
+ import { optionalAuth, requireAuth } from '../middleware/auth.js';
16
+ import {
17
+ validateBody,
18
+ validateParams,
19
+ validateQuery,
20
+ SkillNameParamSchema,
21
+ SearchQuerySchema,
22
+ CreateSkillBodySchema,
23
+ UpdateSkillBodySchema,
24
+ PaginationQuerySchema,
25
+ } from '../middleware/validation.js';
26
+
27
+ export async function skillsRoutes(
28
+ server: FastifyInstance,
29
+ _opts: FastifyPluginOptions
30
+ ): Promise<void> {
31
+ // Search skills
32
+ // GET /v1/skills?q=...&category=...&author=...
33
+ server.get('/', {
34
+ preHandler: [
35
+ optionalAuth,
36
+ validateQuery(SearchQuerySchema),
37
+ ],
38
+ handler: searchSkills,
39
+ });
40
+
41
+ // Get skill by name
42
+ // GET /v1/skills/:name
43
+ server.get('/:name', {
44
+ preHandler: [
45
+ optionalAuth,
46
+ validateParams(SkillNameParamSchema),
47
+ ],
48
+ handler: getSkillByName,
49
+ });
50
+
51
+ // Get skill content (fetch-on-use)
52
+ // GET /v1/skills/:name/content?version=...
53
+ server.get('/:name/content', {
54
+ preHandler: [
55
+ optionalAuth,
56
+ validateParams(SkillNameParamSchema),
57
+ ],
58
+ handler: getSkillContent,
59
+ });
60
+
61
+ // Create/publish a new skill
62
+ // POST /v1/skills
63
+ server.post('/', {
64
+ preHandler: [
65
+ requireAuth,
66
+ validateBody(CreateSkillBodySchema),
67
+ ],
68
+ handler: createSkill,
69
+ });
70
+
71
+ // Update skill metadata
72
+ // PATCH /v1/skills/:name
73
+ server.patch('/:name', {
74
+ preHandler: [
75
+ requireAuth,
76
+ validateParams(SkillNameParamSchema),
77
+ validateBody(UpdateSkillBodySchema),
78
+ ],
79
+ handler: updateSkill,
80
+ });
81
+
82
+ // Delete skill
83
+ // DELETE /v1/skills/:name?hard=true
84
+ server.delete('/:name', {
85
+ preHandler: [
86
+ requireAuth,
87
+ validateParams(SkillNameParamSchema),
88
+ ],
89
+ handler: deleteSkill,
90
+ });
91
+
92
+ // Get derived skills
93
+ // GET /v1/skills/:name/derived
94
+ server.get('/:name/derived', {
95
+ preHandler: [
96
+ optionalAuth,
97
+ validateParams(SkillNameParamSchema),
98
+ validateQuery(PaginationQuerySchema),
99
+ ],
100
+ handler: getDerivedSkills,
101
+ });
102
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Open Skills Hub - Versions Routes
3
+ */
4
+
5
+ import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
6
+ import { z } from 'zod';
7
+ import {
8
+ getVersions,
9
+ getVersion,
10
+ publishVersion,
11
+ deprecateVersion,
12
+ deleteVersion,
13
+ } from '../controllers/versions.js';
14
+ import { optionalAuth, requireAuth } from '../middleware/auth.js';
15
+ import {
16
+ validateBody,
17
+ validateParams,
18
+ validateQuery,
19
+ SkillNameParamSchema,
20
+ SkillVersionParamSchema,
21
+ PublishVersionBodySchema,
22
+ PaginationQuerySchema,
23
+ } from '../middleware/validation.js';
24
+
25
+ // Extended query schema for versions
26
+ const VersionsQuerySchema = PaginationQuerySchema.extend({
27
+ includeDeprecated: z.string().optional(),
28
+ });
29
+
30
+ // Deprecate body schema
31
+ const DeprecateBodySchema = z.object({
32
+ message: z.string().optional(),
33
+ });
34
+
35
+ export async function versionsRoutes(
36
+ server: FastifyInstance,
37
+ _opts: FastifyPluginOptions
38
+ ): Promise<void> {
39
+ // Get all versions of a skill
40
+ // GET /v1/skills/:name/versions
41
+ server.get('/:name/versions', {
42
+ preHandler: [
43
+ optionalAuth,
44
+ validateParams(SkillNameParamSchema),
45
+ validateQuery(VersionsQuerySchema),
46
+ ],
47
+ handler: getVersions,
48
+ });
49
+
50
+ // Get a specific version
51
+ // GET /v1/skills/:name/versions/:version
52
+ server.get('/:name/versions/:version', {
53
+ preHandler: [
54
+ optionalAuth,
55
+ validateParams(SkillVersionParamSchema),
56
+ ],
57
+ handler: getVersion,
58
+ });
59
+
60
+ // Publish a new version
61
+ // POST /v1/skills/:name/versions
62
+ server.post('/:name/versions', {
63
+ preHandler: [
64
+ requireAuth,
65
+ validateParams(SkillNameParamSchema),
66
+ validateBody(PublishVersionBodySchema),
67
+ ],
68
+ handler: publishVersion,
69
+ });
70
+
71
+ // Deprecate a version
72
+ // POST /v1/skills/:name/versions/:version/deprecate
73
+ server.post('/:name/versions/:version/deprecate', {
74
+ preHandler: [
75
+ requireAuth,
76
+ validateParams(SkillVersionParamSchema),
77
+ validateBody(DeprecateBodySchema),
78
+ ],
79
+ handler: deprecateVersion,
80
+ });
81
+
82
+ // Delete/yank a version
83
+ // DELETE /v1/skills/:name/versions/:version
84
+ server.delete('/:name/versions/:version', {
85
+ preHandler: [
86
+ requireAuth,
87
+ validateParams(SkillVersionParamSchema),
88
+ ],
89
+ handler: deleteVersion,
90
+ });
91
+ }