@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,489 @@
1
+ /**
2
+ * Open Skills Hub - Skills Controller
3
+ */
4
+
5
+ import type { FastifyRequest, FastifyReply } from 'fastify';
6
+ import {
7
+ getStorage,
8
+ generateUUID,
9
+ now,
10
+ sha256,
11
+ buildSkillFullName,
12
+ parseSkillFullName,
13
+ AppError,
14
+ ErrorCodes,
15
+ } from '@open-skills-hub/core';
16
+ import type { Skill, Version, SkillContent, AuditLog } from '@open-skills-hub/core';
17
+ import { sendSuccess, sendError } from '../middleware/error.js';
18
+ import { logger } from '../middleware/logger.js';
19
+ import type {
20
+ SkillNameParam,
21
+ SkillVersionParam,
22
+ CreateSkillBody,
23
+ UpdateSkillBody,
24
+ PublishVersionBody,
25
+ PaginationQuery,
26
+ SearchQuery,
27
+ } from '../middleware/validation.js';
28
+ // Re-export for use in routes
29
+ export type { SearchQuery };
30
+
31
+ /**
32
+ * Search skills
33
+ */
34
+ export async function searchSkills(
35
+ request: FastifyRequest<{ Querystring: SearchQuery }>,
36
+ reply: FastifyReply
37
+ ): Promise<void> {
38
+ const storage = await getStorage();
39
+ const { q, category, author, sort, order, cursor, limit } = request.query;
40
+
41
+ const result = await storage.searchSkills({
42
+ query: q,
43
+ category,
44
+ author,
45
+ sort,
46
+ order,
47
+ cursor,
48
+ limit,
49
+ });
50
+
51
+ sendSuccess(request, reply, result.items, 200, result.pagination);
52
+ }
53
+
54
+ /**
55
+ * Get skill by name
56
+ */
57
+ export async function getSkillByName(
58
+ request: FastifyRequest<{ Params: SkillNameParam }>,
59
+ reply: FastifyReply
60
+ ): Promise<void> {
61
+ const storage = await getStorage();
62
+ const { name } = request.params;
63
+
64
+ // Parse name (might be scoped like @scope/name)
65
+ const { scope, name: skillName } = parseSkillFullName(name);
66
+ const fullName = buildSkillFullName(skillName, scope);
67
+
68
+ const skill = await storage.getSkillByName(fullName);
69
+ if (!skill) {
70
+ throw new AppError(
71
+ ErrorCodes.SKILL_NOT_FOUND,
72
+ `Skill '${fullName}' not found`,
73
+ 404
74
+ );
75
+ }
76
+
77
+ // Get latest version info
78
+ const latestVersion = await storage.getLatestVersion(skill.id);
79
+
80
+ sendSuccess(request, reply, {
81
+ ...skill,
82
+ latestVersionDetails: latestVersion ? {
83
+ version: latestVersion.version,
84
+ publishedAt: latestVersion.publishedAt,
85
+ securityLevel: latestVersion.securityLevel,
86
+ } : undefined,
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Get skill content (fetch-on-use)
92
+ */
93
+ export async function getSkillContent(
94
+ request: FastifyRequest<{ Params: SkillNameParam; Querystring: { version?: string } }>,
95
+ reply: FastifyReply
96
+ ): Promise<void> {
97
+ const storage = await getStorage();
98
+ const { name } = request.params;
99
+ const { version: requestedVersion } = request.query;
100
+
101
+ // Parse name
102
+ const { scope, name: skillName } = parseSkillFullName(name);
103
+ const fullName = buildSkillFullName(skillName, scope);
104
+
105
+ const skill = await storage.getSkillByName(fullName);
106
+ if (!skill) {
107
+ throw new AppError(
108
+ ErrorCodes.SKILL_NOT_FOUND,
109
+ `Skill '${fullName}' not found`,
110
+ 404
111
+ );
112
+ }
113
+
114
+ // Get version
115
+ let version: Version | null;
116
+ if (requestedVersion) {
117
+ version = await storage.getVersion(skill.id, requestedVersion);
118
+ if (!version) {
119
+ throw new AppError(
120
+ ErrorCodes.VERSION_NOT_FOUND,
121
+ `Version '${requestedVersion}' not found for skill '${fullName}'`,
122
+ 404
123
+ );
124
+ }
125
+ } else {
126
+ version = await storage.getLatestVersion(skill.id);
127
+ if (!version) {
128
+ throw new AppError(
129
+ ErrorCodes.VERSION_NOT_FOUND,
130
+ `No versions found for skill '${fullName}'`,
131
+ 404
132
+ );
133
+ }
134
+ }
135
+
136
+ // Increment use counters
137
+ await Promise.all([
138
+ storage.incrementSkillUses(skill.id),
139
+ storage.incrementVersionUses(version.id),
140
+ ]);
141
+
142
+ // Record use
143
+ const useRecord = {
144
+ id: generateUUID(),
145
+ skillId: skill.id,
146
+ versionId: version.id,
147
+ userId: request.user?.id,
148
+ source: 'api' as const,
149
+ cacheHit: false,
150
+ clientVersion: request.headers['x-client-version'] as string | undefined,
151
+ platform: request.headers['x-platform'] as string | undefined,
152
+ arch: request.headers['x-arch'] as string | undefined,
153
+ ip: request.ip,
154
+ userAgent: request.headers['user-agent'],
155
+ createdAt: now(),
156
+ };
157
+ await storage.createUseRecord(useRecord);
158
+
159
+ sendSuccess(request, reply, {
160
+ skill: {
161
+ name: skill.fullName,
162
+ displayName: skill.displayName,
163
+ description: skill.description,
164
+ securityLevel: skill.securityLevel,
165
+ },
166
+ version: version.version,
167
+ content: version.content,
168
+ packageHash: version.packageHash,
169
+ securityScore: version.securityScore,
170
+ securityLevel: version.securityLevel,
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Create/publish a new skill
176
+ */
177
+ export async function createSkill(
178
+ request: FastifyRequest<{ Body: CreateSkillBody }>,
179
+ reply: FastifyReply
180
+ ): Promise<void> {
181
+ const storage = await getStorage();
182
+ const body = request.body;
183
+ const userId = request.user?.id;
184
+
185
+ // Build full name
186
+ const fullName = buildSkillFullName(body.name, body.scope);
187
+
188
+ // Check if skill already exists
189
+ const existing = await storage.getSkillByName(fullName);
190
+ if (existing) {
191
+ throw new AppError(
192
+ ErrorCodes.SKILL_EXISTS,
193
+ `Skill '${fullName}' already exists`,
194
+ 409
195
+ );
196
+ }
197
+
198
+ // Calculate content hash
199
+ const contentString = JSON.stringify(body.content);
200
+ const contentHash = sha256(contentString);
201
+
202
+ const skillId = generateUUID();
203
+ const versionId = generateUUID();
204
+ const timestamp = now();
205
+
206
+ // Extract author from frontmatter or use userId
207
+ const author = body.content.frontmatter.author || userId || 'anonymous';
208
+ const ownerType = (body.scope || author.startsWith('@')) ? 'organization' : 'user';
209
+
210
+ // Create skill
211
+ const skill: Skill = {
212
+ id: skillId,
213
+ name: body.name,
214
+ scope: body.scope,
215
+ fullName,
216
+ ownerId: author,
217
+ ownerType,
218
+ displayName: body.displayName,
219
+ description: body.description,
220
+ category: body.category,
221
+ keywords: body.keywords,
222
+ license: body.license,
223
+ repository: body.repository,
224
+ homepage: body.homepage,
225
+ latestVersion: body.version,
226
+ latestVersionId: versionId,
227
+ visibility: body.visibility,
228
+ status: 'active',
229
+ stats: { totalUses: 0, weeklyUses: 0, monthlyUses: 0, versionCount: 1, derivationCount: 0 },
230
+ rating: { average: 0, count: 0 },
231
+ createdAt: timestamp,
232
+ updatedAt: timestamp,
233
+ publishedAt: timestamp,
234
+ };
235
+
236
+ // Create version
237
+ const version: Version = {
238
+ id: versionId,
239
+ skillId,
240
+ version: body.version,
241
+ tag: 'latest',
242
+ content: body.content,
243
+ packageUrl: `local://${fullName}/${body.version}`,
244
+ packageSize: contentString.length,
245
+ packageHash: contentHash,
246
+ changelog: body.changelog,
247
+ status: 'published',
248
+ publishedBy: author,
249
+ uses: 0,
250
+ createdAt: timestamp,
251
+ publishedAt: timestamp,
252
+ };
253
+
254
+ // Save to storage
255
+ await storage.createSkill(skill);
256
+ await storage.createVersion(version);
257
+
258
+ // Create audit log
259
+ const auditLog: AuditLog = {
260
+ id: generateUUID(),
261
+ timestamp,
262
+ eventType: 'skill.published',
263
+ actor: {
264
+ type: request.user?.type ?? 'user',
265
+ id: userId,
266
+ username: request.user?.username,
267
+ ip: request.ip,
268
+ userAgent: request.headers['user-agent'],
269
+ },
270
+ resource: {
271
+ type: 'skill',
272
+ id: skillId,
273
+ name: fullName,
274
+ version: body.version,
275
+ },
276
+ action: 'create',
277
+ result: 'success',
278
+ details: {
279
+ visibility: body.visibility,
280
+ category: body.category,
281
+ },
282
+ };
283
+ await storage.createAuditLog(auditLog);
284
+
285
+ logger.info('Skill published', { skillId, fullName, version: body.version });
286
+
287
+ sendSuccess(request, reply, { skill, version }, 201);
288
+ }
289
+
290
+ /**
291
+ * Update skill metadata
292
+ */
293
+ export async function updateSkill(
294
+ request: FastifyRequest<{ Params: SkillNameParam; Body: UpdateSkillBody }>,
295
+ reply: FastifyReply
296
+ ): Promise<void> {
297
+ const storage = await getStorage();
298
+ const { name } = request.params;
299
+ const updates = request.body;
300
+ const userId = request.user?.id;
301
+
302
+ // Parse name
303
+ const { scope, name: skillName } = parseSkillFullName(name);
304
+ const fullName = buildSkillFullName(skillName, scope);
305
+
306
+ const skill = await storage.getSkillByName(fullName);
307
+ if (!skill) {
308
+ throw new AppError(
309
+ ErrorCodes.SKILL_NOT_FOUND,
310
+ `Skill '${fullName}' not found`,
311
+ 404
312
+ );
313
+ }
314
+
315
+ // Check ownership (simplified - in production, check more thoroughly)
316
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
317
+ throw new AppError(
318
+ ErrorCodes.FORBIDDEN,
319
+ 'You do not have permission to update this skill',
320
+ 403
321
+ );
322
+ }
323
+
324
+ // Build update object (remove null values to unset optional fields)
325
+ const updateData: Partial<Skill> = {};
326
+ if (updates.displayName !== undefined) updateData.displayName = updates.displayName;
327
+ if (updates.description !== undefined) updateData.description = updates.description;
328
+ if (updates.category !== undefined) updateData.category = updates.category;
329
+ if (updates.keywords !== undefined) updateData.keywords = updates.keywords;
330
+ if (updates.repository !== undefined) updateData.repository = updates.repository ?? undefined;
331
+ if (updates.homepage !== undefined) updateData.homepage = updates.homepage ?? undefined;
332
+ if (updates.visibility !== undefined) updateData.visibility = updates.visibility;
333
+
334
+ const updated = await storage.updateSkill(skill.id, updateData);
335
+
336
+ if (!updated) {
337
+ throw new AppError(
338
+ ErrorCodes.INTERNAL_ERROR,
339
+ 'Failed to update skill',
340
+ 500
341
+ );
342
+ }
343
+
344
+ // Create audit log
345
+ const auditLog: AuditLog = {
346
+ id: generateUUID(),
347
+ timestamp: now(),
348
+ eventType: 'skill.updated',
349
+ actor: {
350
+ type: request.user?.type ?? 'user',
351
+ id: userId,
352
+ username: request.user?.username,
353
+ ip: request.ip,
354
+ userAgent: request.headers['user-agent'],
355
+ },
356
+ resource: {
357
+ type: 'skill',
358
+ id: skill.id,
359
+ name: fullName,
360
+ },
361
+ action: 'update',
362
+ result: 'success',
363
+ changes: {
364
+ before: skill,
365
+ after: updated,
366
+ },
367
+ };
368
+ await storage.createAuditLog(auditLog);
369
+
370
+ logger.info('Skill updated', { skillId: skill.id, fullName });
371
+
372
+ sendSuccess(request, reply, updated);
373
+ }
374
+
375
+ /**
376
+ * Delete/deprecate a skill
377
+ */
378
+ export async function deleteSkill(
379
+ request: FastifyRequest<{ Params: SkillNameParam; Querystring: { hard?: string } }>,
380
+ reply: FastifyReply
381
+ ): Promise<void> {
382
+ const storage = await getStorage();
383
+ const { name } = request.params;
384
+ const { hard } = request.query;
385
+ const userId = request.user?.id;
386
+
387
+ // Parse name
388
+ const { scope, name: skillName } = parseSkillFullName(name);
389
+ const fullName = buildSkillFullName(skillName, scope);
390
+
391
+ const skill = await storage.getSkillByName(fullName);
392
+ if (!skill) {
393
+ throw new AppError(
394
+ ErrorCodes.SKILL_NOT_FOUND,
395
+ `Skill '${fullName}' not found`,
396
+ 404
397
+ );
398
+ }
399
+
400
+ // Check ownership
401
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
402
+ throw new AppError(
403
+ ErrorCodes.FORBIDDEN,
404
+ 'You do not have permission to delete this skill',
405
+ 403
406
+ );
407
+ }
408
+
409
+ let result: boolean;
410
+ if (hard === 'true') {
411
+ // Hard delete
412
+ result = await storage.deleteSkill(skill.id);
413
+ } else {
414
+ // Soft delete (mark as deleted)
415
+ const updated = await storage.updateSkill(skill.id, { status: 'deleted' });
416
+ result = !!updated;
417
+ }
418
+
419
+ // Create audit log
420
+ const auditLog: AuditLog = {
421
+ id: generateUUID(),
422
+ timestamp: now(),
423
+ eventType: 'skill.deleted',
424
+ actor: {
425
+ type: request.user?.type ?? 'user',
426
+ id: userId,
427
+ username: request.user?.username,
428
+ ip: request.ip,
429
+ userAgent: request.headers['user-agent'],
430
+ },
431
+ resource: {
432
+ type: 'skill',
433
+ id: skill.id,
434
+ name: fullName,
435
+ },
436
+ action: hard === 'true' ? 'hard_delete' : 'soft_delete',
437
+ result: result ? 'success' : 'failure',
438
+ };
439
+ await storage.createAuditLog(auditLog);
440
+
441
+ logger.info('Skill deleted', { skillId: skill.id, fullName, hard: hard === 'true' });
442
+
443
+ sendSuccess(request, reply, { deleted: result });
444
+ }
445
+
446
+ /**
447
+ * Get skills by owner
448
+ */
449
+ export async function getSkillsByOwner(
450
+ request: FastifyRequest<{ Params: { ownerId: string }; Querystring: PaginationQuery }>,
451
+ reply: FastifyReply
452
+ ): Promise<void> {
453
+ const storage = await getStorage();
454
+ const { ownerId } = request.params;
455
+ const { cursor, limit } = request.query;
456
+
457
+ const result = await storage.getSkillsByOwner(ownerId, { cursor, limit });
458
+
459
+ sendSuccess(request, reply, result.items, 200, result.pagination);
460
+ }
461
+
462
+ /**
463
+ * Get derived skills
464
+ */
465
+ export async function getDerivedSkills(
466
+ request: FastifyRequest<{ Params: SkillNameParam; Querystring: PaginationQuery }>,
467
+ reply: FastifyReply
468
+ ): Promise<void> {
469
+ const storage = await getStorage();
470
+ const { name } = request.params;
471
+ const { cursor, limit } = request.query;
472
+
473
+ // Parse name
474
+ const { scope, name: skillName } = parseSkillFullName(name);
475
+ const fullName = buildSkillFullName(skillName, scope);
476
+
477
+ const skill = await storage.getSkillByName(fullName);
478
+ if (!skill) {
479
+ throw new AppError(
480
+ ErrorCodes.SKILL_NOT_FOUND,
481
+ `Skill '${fullName}' not found`,
482
+ 404
483
+ );
484
+ }
485
+
486
+ const result = await storage.getDerivedSkills(skill.id, { cursor, limit });
487
+
488
+ sendSuccess(request, reply, result.items, 200, result.pagination);
489
+ }