@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.
- package/dist/controllers/audit.d.ts +33 -0
- package/dist/controllers/audit.d.ts.map +1 -0
- package/dist/controllers/audit.js +122 -0
- package/dist/controllers/audit.js.map +1 -0
- package/dist/controllers/cache.d.ts +42 -0
- package/dist/controllers/cache.d.ts.map +1 -0
- package/dist/controllers/cache.js +247 -0
- package/dist/controllers/cache.js.map +1 -0
- package/dist/controllers/feedback.d.ts +44 -0
- package/dist/controllers/feedback.d.ts.map +1 -0
- package/dist/controllers/feedback.js +216 -0
- package/dist/controllers/feedback.js.map +1 -0
- package/dist/controllers/index.d.ts +9 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +9 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/skills.d.ts +66 -0
- package/dist/controllers/skills.d.ts.map +1 -0
- package/dist/controllers/skills.js +355 -0
- package/dist/controllers/skills.js.map +1 -0
- package/dist/controllers/versions.d.ts +43 -0
- package/dist/controllers/versions.d.ts.map +1 -0
- package/dist/controllers/versions.js +298 -0
- package/dist/controllers/versions.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +34 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +148 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error.d.ts +26 -0
- package/dist/middleware/error.d.ts.map +1 -0
- package/dist/middleware/error.js +102 -0
- package/dist/middleware/error.js.map +1 -0
- package/dist/middleware/index.d.ts +8 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +8 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +19 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +54 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/validation.d.ts +671 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +225 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/routes/audit.d.ts +6 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +54 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/cache.d.ts +6 -0
- package/dist/routes/cache.d.ts.map +1 -0
- package/dist/routes/cache.js +70 -0
- package/dist/routes/cache.js.map +1 -0
- package/dist/routes/feedback.d.ts +6 -0
- package/dist/routes/feedback.d.ts.map +1 -0
- package/dist/routes/feedback.js +68 -0
- package/dist/routes/feedback.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +122 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/scan.d.ts +8 -0
- package/dist/routes/scan.d.ts.map +1 -0
- package/dist/routes/scan.js +315 -0
- package/dist/routes/scan.js.map +1 -0
- package/dist/routes/search.d.ts +6 -0
- package/dist/routes/search.d.ts.map +1 -0
- package/dist/routes/search.js +44 -0
- package/dist/routes/search.js.map +1 -0
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +74 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/versions.d.ts +6 -0
- package/dist/routes/versions.d.ts.map +1 -0
- package/dist/routes/versions.js +66 -0
- package/dist/routes/versions.js.map +1 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +166 -0
- package/dist/server.js.map +1 -0
- package/package.json +42 -0
- package/src/controllers/audit.ts +175 -0
- package/src/controllers/cache.ts +344 -0
- package/src/controllers/feedback.ts +309 -0
- package/src/controllers/index.ts +9 -0
- package/src/controllers/skills.ts +489 -0
- package/src/controllers/versions.ts +427 -0
- package/src/index.ts +87 -0
- package/src/middleware/auth.ts +219 -0
- package/src/middleware/error.ts +180 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/logger.ts +71 -0
- package/src/middleware/validation.ts +270 -0
- package/src/routes/audit.ts +74 -0
- package/src/routes/cache.ts +93 -0
- package/src/routes/feedback.ts +93 -0
- package/src/routes/health.ts +151 -0
- package/src/routes/index.ts +12 -0
- package/src/routes/scan.ts +428 -0
- package/src/routes/search.ts +51 -0
- package/src/routes/skills.ts +102 -0
- package/src/routes/versions.ts +91 -0
- package/src/server.ts +205 -0
- 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 @@
|
|
|
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 @@
|
|
|
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
|