@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,216 @@
1
+ /**
2
+ * Open Skills Hub - Feedback Controller
3
+ */
4
+ import { getStorage, generateUUID, now, hashIP, buildSkillFullName, parseSkillFullName, AppError, ErrorCodes, } from '@open-skills-hub/core';
5
+ import { sendSuccess } from '../middleware/error.js';
6
+ import { logger } from '../middleware/logger.js';
7
+ /**
8
+ * Submit feedback for a skill
9
+ */
10
+ export async function submitFeedback(request, reply) {
11
+ const storage = await getStorage();
12
+ const body = request.body;
13
+ // Parse name
14
+ const { scope, name: skillName } = parseSkillFullName(body.skillName);
15
+ const fullName = buildSkillFullName(skillName, scope);
16
+ const skill = await storage.getSkillByName(fullName);
17
+ if (!skill) {
18
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
19
+ }
20
+ // Verify version exists
21
+ const version = await storage.getVersion(skill.id, body.skillVersion);
22
+ if (!version) {
23
+ throw new AppError(ErrorCodes.VERSION_NOT_FOUND, `Version '${body.skillVersion}' not found for skill '${fullName}'`, 404);
24
+ }
25
+ const feedbackId = generateUUID();
26
+ const timestamp = now();
27
+ const feedback = {
28
+ id: feedbackId,
29
+ skillId: skill.id,
30
+ skillVersion: body.skillVersion,
31
+ feedbackType: body.feedbackType,
32
+ rating: body.rating,
33
+ comment: body.comment,
34
+ context: body.context ?? {},
35
+ status: 'pending',
36
+ sourceIpHash: hashIP(request.ip),
37
+ createdAt: timestamp,
38
+ };
39
+ await storage.createFeedback(feedback);
40
+ // Update skill rating if this is a rating feedback
41
+ if (body.rating) {
42
+ const feedbacks = await storage.getFeedbacks(skill.id, { limit: 1000 });
43
+ const ratings = feedbacks.items.map(f => f.rating).filter((r) => r !== undefined);
44
+ const averageRating = ratings.length > 0
45
+ ? ratings.reduce((sum, r) => sum + r, 0) / ratings.length
46
+ : 0;
47
+ await storage.updateSkill(skill.id, {
48
+ rating: {
49
+ average: Math.round(averageRating * 10) / 10,
50
+ count: ratings.length,
51
+ },
52
+ });
53
+ }
54
+ // Create audit log
55
+ const auditLog = {
56
+ id: generateUUID(),
57
+ timestamp,
58
+ eventType: 'feedback.submitted',
59
+ actor: {
60
+ type: request.user?.type ?? 'user',
61
+ id: request.user?.id,
62
+ username: request.user?.username,
63
+ ip: request.ip,
64
+ userAgent: request.headers['user-agent'],
65
+ },
66
+ resource: {
67
+ type: 'feedback',
68
+ id: feedbackId,
69
+ name: fullName,
70
+ version: body.skillVersion,
71
+ },
72
+ action: 'create',
73
+ result: 'success',
74
+ details: {
75
+ feedbackType: body.feedbackType,
76
+ rating: body.rating,
77
+ },
78
+ };
79
+ await storage.createAuditLog(auditLog);
80
+ logger.info('Feedback submitted', {
81
+ feedbackId,
82
+ skillName: fullName,
83
+ feedbackType: body.feedbackType,
84
+ rating: body.rating,
85
+ });
86
+ sendSuccess(request, reply, {
87
+ id: feedbackId,
88
+ status: 'pending',
89
+ createdAt: timestamp,
90
+ }, 201);
91
+ }
92
+ /**
93
+ * Get feedbacks for a skill
94
+ */
95
+ export async function getSkillFeedbacks(request, reply) {
96
+ const storage = await getStorage();
97
+ const { name } = request.params;
98
+ const { cursor, limit, feedbackType, status } = request.query;
99
+ // Parse name
100
+ const { scope, name: skillName } = parseSkillFullName(name);
101
+ const fullName = buildSkillFullName(skillName, scope);
102
+ const skill = await storage.getSkillByName(fullName);
103
+ if (!skill) {
104
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
105
+ }
106
+ const result = await storage.getFeedbacks(skill.id, {
107
+ cursor,
108
+ limit,
109
+ feedbackType,
110
+ status,
111
+ });
112
+ // Remove sensitive data
113
+ const feedbacks = result.items.map(f => ({
114
+ id: f.id,
115
+ skillVersion: f.skillVersion,
116
+ feedbackType: f.feedbackType,
117
+ rating: f.rating,
118
+ comment: f.comment,
119
+ context: f.context,
120
+ status: f.status,
121
+ createdAt: f.createdAt,
122
+ }));
123
+ sendSuccess(request, reply, feedbacks, 200, result.pagination);
124
+ }
125
+ /**
126
+ * Get feedback by ID
127
+ */
128
+ export async function getFeedbackById(request, reply) {
129
+ const storage = await getStorage();
130
+ const { id } = request.params;
131
+ const feedback = await storage.getFeedbackById(id);
132
+ if (!feedback) {
133
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, // Using generic not found
134
+ `Feedback '${id}' not found`, 404);
135
+ }
136
+ sendSuccess(request, reply, {
137
+ id: feedback.id,
138
+ skillId: feedback.skillId,
139
+ skillVersion: feedback.skillVersion,
140
+ feedbackType: feedback.feedbackType,
141
+ rating: feedback.rating,
142
+ comment: feedback.comment,
143
+ context: feedback.context,
144
+ status: feedback.status,
145
+ createdAt: feedback.createdAt,
146
+ });
147
+ }
148
+ /**
149
+ * Review feedback (admin)
150
+ */
151
+ export async function reviewFeedback(request, reply) {
152
+ const storage = await getStorage();
153
+ const { id } = request.params;
154
+ const { status, notes } = request.body;
155
+ const userId = request.user?.id;
156
+ const feedback = await storage.getFeedbackById(id);
157
+ if (!feedback) {
158
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Feedback '${id}' not found`, 404);
159
+ }
160
+ const updated = await storage.updateFeedback(id, {
161
+ status,
162
+ reviewerNotes: notes,
163
+ reviewedAt: now(),
164
+ reviewedBy: userId,
165
+ });
166
+ logger.info('Feedback reviewed', {
167
+ feedbackId: id,
168
+ status,
169
+ reviewedBy: userId,
170
+ });
171
+ sendSuccess(request, reply, {
172
+ id: updated?.id,
173
+ status: updated?.status,
174
+ reviewerNotes: updated?.reviewerNotes,
175
+ reviewedAt: updated?.reviewedAt,
176
+ });
177
+ }
178
+ /**
179
+ * Get feedback statistics for a skill
180
+ */
181
+ export async function getFeedbackStats(request, reply) {
182
+ const storage = await getStorage();
183
+ const { name } = request.params;
184
+ // Parse name
185
+ const { scope, name: skillName } = parseSkillFullName(name);
186
+ const fullName = buildSkillFullName(skillName, scope);
187
+ const skill = await storage.getSkillByName(fullName);
188
+ if (!skill) {
189
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
190
+ }
191
+ // Get all feedbacks
192
+ const result = await storage.getFeedbacks(skill.id, { limit: 1000 });
193
+ const feedbacks = result.items;
194
+ // Calculate stats
195
+ const stats = {
196
+ total: feedbacks.length,
197
+ byType: {
198
+ success: feedbacks.filter(f => f.feedbackType === 'success').length,
199
+ failure: feedbacks.filter(f => f.feedbackType === 'failure').length,
200
+ suggestion: feedbacks.filter(f => f.feedbackType === 'suggestion').length,
201
+ bug: feedbacks.filter(f => f.feedbackType === 'bug').length,
202
+ },
203
+ byStatus: {
204
+ pending: feedbacks.filter(f => f.status === 'pending').length,
205
+ reviewed: feedbacks.filter(f => f.status === 'reviewed').length,
206
+ accepted: feedbacks.filter(f => f.status === 'accepted').length,
207
+ rejected: feedbacks.filter(f => f.status === 'rejected').length,
208
+ },
209
+ rating: skill.rating,
210
+ successRate: feedbacks.length > 0
211
+ ? feedbacks.filter(f => f.feedbackType === 'success').length / feedbacks.length
212
+ : 0,
213
+ };
214
+ sendSuccess(request, reply, stats);
215
+ }
216
+ //# sourceMappingURL=feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.js","sourceRoot":"","sources":["../../src/controllers/feedback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Open Skills Hub - Controllers Exports
3
+ */
4
+ export * from './skills.js';
5
+ export * from './versions.js';
6
+ export * from './feedback.js';
7
+ export * from './audit.js';
8
+ export * from './cache.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controllers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Open Skills Hub - Controllers Exports
3
+ */
4
+ export * from './skills.js';
5
+ export * from './versions.js';
6
+ export * from './feedback.js';
7
+ export * from './audit.js';
8
+ export * from './cache.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/controllers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Open Skills Hub - Skills Controller
3
+ */
4
+ import type { FastifyRequest, FastifyReply } from 'fastify';
5
+ import type { SkillNameParam, CreateSkillBody, UpdateSkillBody, PaginationQuery, SearchQuery } from '../middleware/validation.js';
6
+ export type { SearchQuery };
7
+ /**
8
+ * Search skills
9
+ */
10
+ export declare function searchSkills(request: FastifyRequest<{
11
+ Querystring: SearchQuery;
12
+ }>, reply: FastifyReply): Promise<void>;
13
+ /**
14
+ * Get skill by name
15
+ */
16
+ export declare function getSkillByName(request: FastifyRequest<{
17
+ Params: SkillNameParam;
18
+ }>, reply: FastifyReply): Promise<void>;
19
+ /**
20
+ * Get skill content (fetch-on-use)
21
+ */
22
+ export declare function getSkillContent(request: FastifyRequest<{
23
+ Params: SkillNameParam;
24
+ Querystring: {
25
+ version?: string;
26
+ };
27
+ }>, reply: FastifyReply): Promise<void>;
28
+ /**
29
+ * Create/publish a new skill
30
+ */
31
+ export declare function createSkill(request: FastifyRequest<{
32
+ Body: CreateSkillBody;
33
+ }>, reply: FastifyReply): Promise<void>;
34
+ /**
35
+ * Update skill metadata
36
+ */
37
+ export declare function updateSkill(request: FastifyRequest<{
38
+ Params: SkillNameParam;
39
+ Body: UpdateSkillBody;
40
+ }>, reply: FastifyReply): Promise<void>;
41
+ /**
42
+ * Delete/deprecate a skill
43
+ */
44
+ export declare function deleteSkill(request: FastifyRequest<{
45
+ Params: SkillNameParam;
46
+ Querystring: {
47
+ hard?: string;
48
+ };
49
+ }>, reply: FastifyReply): Promise<void>;
50
+ /**
51
+ * Get skills by owner
52
+ */
53
+ export declare function getSkillsByOwner(request: FastifyRequest<{
54
+ Params: {
55
+ ownerId: string;
56
+ };
57
+ Querystring: PaginationQuery;
58
+ }>, reply: FastifyReply): Promise<void>;
59
+ /**
60
+ * Get derived skills
61
+ */
62
+ export declare function getDerivedSkills(request: FastifyRequest<{
63
+ Params: SkillNameParam;
64
+ Querystring: PaginationQuery;
65
+ }>, reply: FastifyReply): Promise<void>;
66
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/controllers/skills.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAc5D,OAAO,KAAK,EACV,cAAc,EAEd,eAAe,EACf,eAAe,EAEf,eAAe,EACf,WAAW,EACZ,MAAM,6BAA6B,CAAC;AAErC,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B;;GAEG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,CAAC;IAAE,WAAW,EAAE,WAAW,CAAA;CAAE,CAAC,EACrD,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,CAAC,EACnD,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Bf;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,WAAW,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,EACtF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Ef;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,CAAC;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAClD,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Gf;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAC1E,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CA6Ef;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,WAAW,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,EACnF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CA+Df;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,WAAW,EAAE,eAAe,CAAA;CAAE,CAAC,EACtF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,cAAc,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,WAAW,EAAE,eAAe,CAAA;CAAE,CAAC,EACjF,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Open Skills Hub - Skills Controller
3
+ */
4
+ import { getStorage, generateUUID, now, sha256, buildSkillFullName, parseSkillFullName, AppError, ErrorCodes, } from '@open-skills-hub/core';
5
+ import { sendSuccess } from '../middleware/error.js';
6
+ import { logger } from '../middleware/logger.js';
7
+ /**
8
+ * Search skills
9
+ */
10
+ export async function searchSkills(request, reply) {
11
+ const storage = await getStorage();
12
+ const { q, category, author, sort, order, cursor, limit } = request.query;
13
+ const result = await storage.searchSkills({
14
+ query: q,
15
+ category,
16
+ author,
17
+ sort,
18
+ order,
19
+ cursor,
20
+ limit,
21
+ });
22
+ sendSuccess(request, reply, result.items, 200, result.pagination);
23
+ }
24
+ /**
25
+ * Get skill by name
26
+ */
27
+ export async function getSkillByName(request, reply) {
28
+ const storage = await getStorage();
29
+ const { name } = request.params;
30
+ // Parse name (might be scoped like @scope/name)
31
+ const { scope, name: skillName } = parseSkillFullName(name);
32
+ const fullName = buildSkillFullName(skillName, scope);
33
+ const skill = await storage.getSkillByName(fullName);
34
+ if (!skill) {
35
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
36
+ }
37
+ // Get latest version info
38
+ const latestVersion = await storage.getLatestVersion(skill.id);
39
+ sendSuccess(request, reply, {
40
+ ...skill,
41
+ latestVersionDetails: latestVersion ? {
42
+ version: latestVersion.version,
43
+ publishedAt: latestVersion.publishedAt,
44
+ securityLevel: latestVersion.securityLevel,
45
+ } : undefined,
46
+ });
47
+ }
48
+ /**
49
+ * Get skill content (fetch-on-use)
50
+ */
51
+ export async function getSkillContent(request, reply) {
52
+ const storage = await getStorage();
53
+ const { name } = request.params;
54
+ const { version: requestedVersion } = request.query;
55
+ // Parse name
56
+ const { scope, name: skillName } = parseSkillFullName(name);
57
+ const fullName = buildSkillFullName(skillName, scope);
58
+ const skill = await storage.getSkillByName(fullName);
59
+ if (!skill) {
60
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
61
+ }
62
+ // Get version
63
+ let version;
64
+ if (requestedVersion) {
65
+ version = await storage.getVersion(skill.id, requestedVersion);
66
+ if (!version) {
67
+ throw new AppError(ErrorCodes.VERSION_NOT_FOUND, `Version '${requestedVersion}' not found for skill '${fullName}'`, 404);
68
+ }
69
+ }
70
+ else {
71
+ version = await storage.getLatestVersion(skill.id);
72
+ if (!version) {
73
+ throw new AppError(ErrorCodes.VERSION_NOT_FOUND, `No versions found for skill '${fullName}'`, 404);
74
+ }
75
+ }
76
+ // Increment use counters
77
+ await Promise.all([
78
+ storage.incrementSkillUses(skill.id),
79
+ storage.incrementVersionUses(version.id),
80
+ ]);
81
+ // Record use
82
+ const useRecord = {
83
+ id: generateUUID(),
84
+ skillId: skill.id,
85
+ versionId: version.id,
86
+ userId: request.user?.id,
87
+ source: 'api',
88
+ cacheHit: false,
89
+ clientVersion: request.headers['x-client-version'],
90
+ platform: request.headers['x-platform'],
91
+ arch: request.headers['x-arch'],
92
+ ip: request.ip,
93
+ userAgent: request.headers['user-agent'],
94
+ createdAt: now(),
95
+ };
96
+ await storage.createUseRecord(useRecord);
97
+ sendSuccess(request, reply, {
98
+ skill: {
99
+ name: skill.fullName,
100
+ displayName: skill.displayName,
101
+ description: skill.description,
102
+ securityLevel: skill.securityLevel,
103
+ },
104
+ version: version.version,
105
+ content: version.content,
106
+ packageHash: version.packageHash,
107
+ securityScore: version.securityScore,
108
+ securityLevel: version.securityLevel,
109
+ });
110
+ }
111
+ /**
112
+ * Create/publish a new skill
113
+ */
114
+ export async function createSkill(request, reply) {
115
+ const storage = await getStorage();
116
+ const body = request.body;
117
+ const userId = request.user?.id;
118
+ // Build full name
119
+ const fullName = buildSkillFullName(body.name, body.scope);
120
+ // Check if skill already exists
121
+ const existing = await storage.getSkillByName(fullName);
122
+ if (existing) {
123
+ throw new AppError(ErrorCodes.SKILL_EXISTS, `Skill '${fullName}' already exists`, 409);
124
+ }
125
+ // Calculate content hash
126
+ const contentString = JSON.stringify(body.content);
127
+ const contentHash = sha256(contentString);
128
+ const skillId = generateUUID();
129
+ const versionId = generateUUID();
130
+ const timestamp = now();
131
+ // Extract author from frontmatter or use userId
132
+ const author = body.content.frontmatter.author || userId || 'anonymous';
133
+ const ownerType = (body.scope || author.startsWith('@')) ? 'organization' : 'user';
134
+ // Create skill
135
+ const skill = {
136
+ id: skillId,
137
+ name: body.name,
138
+ scope: body.scope,
139
+ fullName,
140
+ ownerId: author,
141
+ ownerType,
142
+ displayName: body.displayName,
143
+ description: body.description,
144
+ category: body.category,
145
+ keywords: body.keywords,
146
+ license: body.license,
147
+ repository: body.repository,
148
+ homepage: body.homepage,
149
+ latestVersion: body.version,
150
+ latestVersionId: versionId,
151
+ visibility: body.visibility,
152
+ status: 'active',
153
+ stats: { totalUses: 0, weeklyUses: 0, monthlyUses: 0, versionCount: 1, derivationCount: 0 },
154
+ rating: { average: 0, count: 0 },
155
+ createdAt: timestamp,
156
+ updatedAt: timestamp,
157
+ publishedAt: timestamp,
158
+ };
159
+ // Create version
160
+ const version = {
161
+ id: versionId,
162
+ skillId,
163
+ version: body.version,
164
+ tag: 'latest',
165
+ content: body.content,
166
+ packageUrl: `local://${fullName}/${body.version}`,
167
+ packageSize: contentString.length,
168
+ packageHash: contentHash,
169
+ changelog: body.changelog,
170
+ status: 'published',
171
+ publishedBy: author,
172
+ uses: 0,
173
+ createdAt: timestamp,
174
+ publishedAt: timestamp,
175
+ };
176
+ // Save to storage
177
+ await storage.createSkill(skill);
178
+ await storage.createVersion(version);
179
+ // Create audit log
180
+ const auditLog = {
181
+ id: generateUUID(),
182
+ timestamp,
183
+ eventType: 'skill.published',
184
+ actor: {
185
+ type: request.user?.type ?? 'user',
186
+ id: userId,
187
+ username: request.user?.username,
188
+ ip: request.ip,
189
+ userAgent: request.headers['user-agent'],
190
+ },
191
+ resource: {
192
+ type: 'skill',
193
+ id: skillId,
194
+ name: fullName,
195
+ version: body.version,
196
+ },
197
+ action: 'create',
198
+ result: 'success',
199
+ details: {
200
+ visibility: body.visibility,
201
+ category: body.category,
202
+ },
203
+ };
204
+ await storage.createAuditLog(auditLog);
205
+ logger.info('Skill published', { skillId, fullName, version: body.version });
206
+ sendSuccess(request, reply, { skill, version }, 201);
207
+ }
208
+ /**
209
+ * Update skill metadata
210
+ */
211
+ export async function updateSkill(request, reply) {
212
+ const storage = await getStorage();
213
+ const { name } = request.params;
214
+ const updates = request.body;
215
+ const userId = request.user?.id;
216
+ // Parse name
217
+ const { scope, name: skillName } = parseSkillFullName(name);
218
+ const fullName = buildSkillFullName(skillName, scope);
219
+ const skill = await storage.getSkillByName(fullName);
220
+ if (!skill) {
221
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
222
+ }
223
+ // Check ownership (simplified - in production, check more thoroughly)
224
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
225
+ throw new AppError(ErrorCodes.FORBIDDEN, 'You do not have permission to update this skill', 403);
226
+ }
227
+ // Build update object (remove null values to unset optional fields)
228
+ const updateData = {};
229
+ if (updates.displayName !== undefined)
230
+ updateData.displayName = updates.displayName;
231
+ if (updates.description !== undefined)
232
+ updateData.description = updates.description;
233
+ if (updates.category !== undefined)
234
+ updateData.category = updates.category;
235
+ if (updates.keywords !== undefined)
236
+ updateData.keywords = updates.keywords;
237
+ if (updates.repository !== undefined)
238
+ updateData.repository = updates.repository ?? undefined;
239
+ if (updates.homepage !== undefined)
240
+ updateData.homepage = updates.homepage ?? undefined;
241
+ if (updates.visibility !== undefined)
242
+ updateData.visibility = updates.visibility;
243
+ const updated = await storage.updateSkill(skill.id, updateData);
244
+ if (!updated) {
245
+ throw new AppError(ErrorCodes.INTERNAL_ERROR, 'Failed to update skill', 500);
246
+ }
247
+ // Create audit log
248
+ const auditLog = {
249
+ id: generateUUID(),
250
+ timestamp: now(),
251
+ eventType: 'skill.updated',
252
+ actor: {
253
+ type: request.user?.type ?? 'user',
254
+ id: userId,
255
+ username: request.user?.username,
256
+ ip: request.ip,
257
+ userAgent: request.headers['user-agent'],
258
+ },
259
+ resource: {
260
+ type: 'skill',
261
+ id: skill.id,
262
+ name: fullName,
263
+ },
264
+ action: 'update',
265
+ result: 'success',
266
+ changes: {
267
+ before: skill,
268
+ after: updated,
269
+ },
270
+ };
271
+ await storage.createAuditLog(auditLog);
272
+ logger.info('Skill updated', { skillId: skill.id, fullName });
273
+ sendSuccess(request, reply, updated);
274
+ }
275
+ /**
276
+ * Delete/deprecate a skill
277
+ */
278
+ export async function deleteSkill(request, reply) {
279
+ const storage = await getStorage();
280
+ const { name } = request.params;
281
+ const { hard } = request.query;
282
+ const userId = request.user?.id;
283
+ // Parse name
284
+ const { scope, name: skillName } = parseSkillFullName(name);
285
+ const fullName = buildSkillFullName(skillName, scope);
286
+ const skill = await storage.getSkillByName(fullName);
287
+ if (!skill) {
288
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
289
+ }
290
+ // Check ownership
291
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
292
+ throw new AppError(ErrorCodes.FORBIDDEN, 'You do not have permission to delete this skill', 403);
293
+ }
294
+ let result;
295
+ if (hard === 'true') {
296
+ // Hard delete
297
+ result = await storage.deleteSkill(skill.id);
298
+ }
299
+ else {
300
+ // Soft delete (mark as deleted)
301
+ const updated = await storage.updateSkill(skill.id, { status: 'deleted' });
302
+ result = !!updated;
303
+ }
304
+ // Create audit log
305
+ const auditLog = {
306
+ id: generateUUID(),
307
+ timestamp: now(),
308
+ eventType: 'skill.deleted',
309
+ actor: {
310
+ type: request.user?.type ?? 'user',
311
+ id: userId,
312
+ username: request.user?.username,
313
+ ip: request.ip,
314
+ userAgent: request.headers['user-agent'],
315
+ },
316
+ resource: {
317
+ type: 'skill',
318
+ id: skill.id,
319
+ name: fullName,
320
+ },
321
+ action: hard === 'true' ? 'hard_delete' : 'soft_delete',
322
+ result: result ? 'success' : 'failure',
323
+ };
324
+ await storage.createAuditLog(auditLog);
325
+ logger.info('Skill deleted', { skillId: skill.id, fullName, hard: hard === 'true' });
326
+ sendSuccess(request, reply, { deleted: result });
327
+ }
328
+ /**
329
+ * Get skills by owner
330
+ */
331
+ export async function getSkillsByOwner(request, reply) {
332
+ const storage = await getStorage();
333
+ const { ownerId } = request.params;
334
+ const { cursor, limit } = request.query;
335
+ const result = await storage.getSkillsByOwner(ownerId, { cursor, limit });
336
+ sendSuccess(request, reply, result.items, 200, result.pagination);
337
+ }
338
+ /**
339
+ * Get derived skills
340
+ */
341
+ export async function getDerivedSkills(request, reply) {
342
+ const storage = await getStorage();
343
+ const { name } = request.params;
344
+ const { cursor, limit } = request.query;
345
+ // Parse name
346
+ const { scope, name: skillName } = parseSkillFullName(name);
347
+ const fullName = buildSkillFullName(skillName, scope);
348
+ const skill = await storage.getSkillByName(fullName);
349
+ if (!skill) {
350
+ throw new AppError(ErrorCodes.SKILL_NOT_FOUND, `Skill '${fullName}' not found`, 404);
351
+ }
352
+ const result = await storage.getDerivedSkills(skill.id, { cursor, limit });
353
+ sendSuccess(request, reply, result.items, 200, result.pagination);
354
+ }
355
+ //# sourceMappingURL=skills.js.map