@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,427 @@
1
+ /**
2
+ * Open Skills Hub - Versions 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 { Version, AuditLog } from '@open-skills-hub/core';
17
+ import { sendSuccess } from '../middleware/error.js';
18
+ import { logger } from '../middleware/logger.js';
19
+ import type {
20
+ SkillNameParam,
21
+ SkillVersionParam,
22
+ PublishVersionBody,
23
+ PaginationQuery,
24
+ } from '../middleware/validation.js';
25
+
26
+ /**
27
+ * Get all versions of a skill
28
+ */
29
+ export async function getVersions(
30
+ request: FastifyRequest<{ Params: SkillNameParam; Querystring: PaginationQuery & { includeDeprecated?: string } }>,
31
+ reply: FastifyReply
32
+ ): Promise<void> {
33
+ const storage = await getStorage();
34
+ const { name } = request.params;
35
+ const { cursor, limit, includeDeprecated } = request.query;
36
+
37
+ // Parse name
38
+ const { scope, name: skillName } = parseSkillFullName(name);
39
+ const fullName = buildSkillFullName(skillName, scope);
40
+
41
+ const skill = await storage.getSkillByName(fullName);
42
+ if (!skill) {
43
+ throw new AppError(
44
+ ErrorCodes.SKILL_NOT_FOUND,
45
+ `Skill '${fullName}' not found`,
46
+ 404
47
+ );
48
+ }
49
+
50
+ const result = await storage.getVersions(skill.id, {
51
+ cursor,
52
+ limit,
53
+ includeDeprecated: includeDeprecated === 'true',
54
+ });
55
+
56
+ // Map versions to public format (exclude full content)
57
+ const versions = result.items.map(v => ({
58
+ id: v.id,
59
+ version: v.version,
60
+ tag: v.tag,
61
+ status: v.status,
62
+ securityScore: v.securityScore,
63
+ securityLevel: v.securityLevel,
64
+ uses: v.uses,
65
+ changelog: v.changelog,
66
+ publishedAt: v.publishedAt,
67
+ deprecatedAt: v.deprecatedAt,
68
+ deprecatedMessage: v.deprecatedMessage,
69
+ }));
70
+
71
+ sendSuccess(request, reply, versions, 200, result.pagination);
72
+ }
73
+
74
+ /**
75
+ * Get a specific version
76
+ */
77
+ export async function getVersion(
78
+ request: FastifyRequest<{ Params: SkillVersionParam }>,
79
+ reply: FastifyReply
80
+ ): Promise<void> {
81
+ const storage = await getStorage();
82
+ const { name, version: versionStr } = request.params;
83
+
84
+ // Parse name
85
+ const { scope, name: skillName } = parseSkillFullName(name);
86
+ const fullName = buildSkillFullName(skillName, scope);
87
+
88
+ const skill = await storage.getSkillByName(fullName);
89
+ if (!skill) {
90
+ throw new AppError(
91
+ ErrorCodes.SKILL_NOT_FOUND,
92
+ `Skill '${fullName}' not found`,
93
+ 404
94
+ );
95
+ }
96
+
97
+ const version = await storage.getVersion(skill.id, versionStr);
98
+ if (!version) {
99
+ throw new AppError(
100
+ ErrorCodes.VERSION_NOT_FOUND,
101
+ `Version '${versionStr}' not found for skill '${fullName}'`,
102
+ 404
103
+ );
104
+ }
105
+
106
+ sendSuccess(request, reply, {
107
+ id: version.id,
108
+ skillId: version.skillId,
109
+ version: version.version,
110
+ tag: version.tag,
111
+ status: version.status,
112
+ securityScore: version.securityScore,
113
+ securityLevel: version.securityLevel,
114
+ uses: version.uses,
115
+ changelog: version.changelog,
116
+ packageSize: version.packageSize,
117
+ packageHash: version.packageHash,
118
+ publishedAt: version.publishedAt,
119
+ deprecatedAt: version.deprecatedAt,
120
+ deprecatedMessage: version.deprecatedMessage,
121
+ content: version.content,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Publish a new version
127
+ */
128
+ export async function publishVersion(
129
+ request: FastifyRequest<{ Params: SkillNameParam; Body: PublishVersionBody }>,
130
+ reply: FastifyReply
131
+ ): Promise<void> {
132
+ const storage = await getStorage();
133
+ const { name } = request.params;
134
+ const body = request.body;
135
+ const userId = request.user?.id;
136
+
137
+ // Parse name
138
+ const { scope, name: skillName } = parseSkillFullName(name);
139
+ const fullName = buildSkillFullName(skillName, scope);
140
+
141
+ const skill = await storage.getSkillByName(fullName);
142
+ if (!skill) {
143
+ throw new AppError(
144
+ ErrorCodes.SKILL_NOT_FOUND,
145
+ `Skill '${fullName}' not found`,
146
+ 404
147
+ );
148
+ }
149
+
150
+ // Check ownership
151
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
152
+ throw new AppError(
153
+ ErrorCodes.FORBIDDEN,
154
+ 'You do not have permission to publish versions for this skill',
155
+ 403
156
+ );
157
+ }
158
+
159
+ // Check if version already exists
160
+ const existingVersion = await storage.getVersion(skill.id, body.version);
161
+ if (existingVersion) {
162
+ throw new AppError(
163
+ ErrorCodes.VERSION_EXISTS,
164
+ `Version '${body.version}' already exists for skill '${fullName}'`,
165
+ 409
166
+ );
167
+ }
168
+
169
+ // Calculate content hash
170
+ const contentString = JSON.stringify(body.content);
171
+ const contentHash = sha256(contentString);
172
+
173
+ const versionId = generateUUID();
174
+ const timestamp = now();
175
+
176
+ // Create version
177
+ const version: Version = {
178
+ id: versionId,
179
+ skillId: skill.id,
180
+ version: body.version,
181
+ tag: body.tag,
182
+ content: body.content,
183
+ packageUrl: `local://${fullName}/${body.version}`,
184
+ packageSize: contentString.length,
185
+ packageHash: contentHash,
186
+ changelog: body.changelog,
187
+ status: 'published',
188
+ publishedBy: userId,
189
+ uses: 0,
190
+ createdAt: timestamp,
191
+ publishedAt: timestamp,
192
+ };
193
+
194
+ await storage.createVersion(version);
195
+
196
+ // Update 'latest' tag on previous version
197
+ const versions = await storage.getVersions(skill.id, { limit: 100 });
198
+ for (const v of versions.items) {
199
+ if (v.id !== versionId && v.tag === 'latest') {
200
+ await storage.updateVersion(v.id, { tag: undefined });
201
+ }
202
+ }
203
+
204
+ // Set latest tag on new version if specified
205
+ if (body.tag === 'latest' || !body.tag) {
206
+ await storage.updateVersion(versionId, { tag: 'latest' });
207
+ }
208
+
209
+ // Update skill's latest version
210
+ await storage.updateSkill(skill.id, {
211
+ latestVersion: body.version,
212
+ latestVersionId: versionId,
213
+ stats: {
214
+ ...skill.stats,
215
+ versionCount: skill.stats.versionCount + 1,
216
+ },
217
+ });
218
+
219
+ // Create audit log
220
+ const auditLog: AuditLog = {
221
+ id: generateUUID(),
222
+ timestamp,
223
+ eventType: 'version.published',
224
+ actor: {
225
+ type: request.user?.type ?? 'user',
226
+ id: userId,
227
+ username: request.user?.username,
228
+ ip: request.ip,
229
+ userAgent: request.headers['user-agent'],
230
+ },
231
+ resource: {
232
+ type: 'version',
233
+ id: versionId,
234
+ name: fullName,
235
+ version: body.version,
236
+ },
237
+ action: 'create',
238
+ result: 'success',
239
+ details: {
240
+ contentHash,
241
+ packageSize: contentString.length,
242
+ },
243
+ };
244
+ await storage.createAuditLog(auditLog);
245
+
246
+ logger.info('Version published', {
247
+ skillId: skill.id,
248
+ fullName,
249
+ version: body.version,
250
+ versionId,
251
+ });
252
+
253
+ sendSuccess(request, reply, {
254
+ id: version.id,
255
+ version: version.version,
256
+ tag: body.tag ?? 'latest',
257
+ publishedAt: version.publishedAt,
258
+ packageHash: version.packageHash,
259
+ }, 201);
260
+ }
261
+
262
+ /**
263
+ * Deprecate a version
264
+ */
265
+ export async function deprecateVersion(
266
+ request: FastifyRequest<{ Params: SkillVersionParam; Body: { message?: string } }>,
267
+ reply: FastifyReply
268
+ ): Promise<void> {
269
+ const storage = await getStorage();
270
+ const { name, version: versionStr } = request.params;
271
+ const { message } = request.body;
272
+ const userId = request.user?.id;
273
+
274
+ // Parse name
275
+ const { scope, name: skillName } = parseSkillFullName(name);
276
+ const fullName = buildSkillFullName(skillName, scope);
277
+
278
+ const skill = await storage.getSkillByName(fullName);
279
+ if (!skill) {
280
+ throw new AppError(
281
+ ErrorCodes.SKILL_NOT_FOUND,
282
+ `Skill '${fullName}' not found`,
283
+ 404
284
+ );
285
+ }
286
+
287
+ // Check ownership
288
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
289
+ throw new AppError(
290
+ ErrorCodes.FORBIDDEN,
291
+ 'You do not have permission to deprecate versions for this skill',
292
+ 403
293
+ );
294
+ }
295
+
296
+ const version = await storage.getVersion(skill.id, versionStr);
297
+ if (!version) {
298
+ throw new AppError(
299
+ ErrorCodes.VERSION_NOT_FOUND,
300
+ `Version '${versionStr}' not found for skill '${fullName}'`,
301
+ 404
302
+ );
303
+ }
304
+
305
+ const deprecatedVersion = await storage.deprecateVersion(
306
+ version.id,
307
+ message ?? 'This version has been deprecated'
308
+ );
309
+
310
+ // Create audit log
311
+ const auditLog: AuditLog = {
312
+ id: generateUUID(),
313
+ timestamp: now(),
314
+ eventType: 'version.deprecated',
315
+ actor: {
316
+ type: request.user?.type ?? 'user',
317
+ id: userId,
318
+ username: request.user?.username,
319
+ ip: request.ip,
320
+ userAgent: request.headers['user-agent'],
321
+ },
322
+ resource: {
323
+ type: 'version',
324
+ id: version.id,
325
+ name: fullName,
326
+ version: versionStr,
327
+ },
328
+ action: 'deprecate',
329
+ result: 'success',
330
+ details: { message },
331
+ };
332
+ await storage.createAuditLog(auditLog);
333
+
334
+ logger.info('Version deprecated', {
335
+ skillId: skill.id,
336
+ fullName,
337
+ version: versionStr,
338
+ });
339
+
340
+ sendSuccess(request, reply, {
341
+ id: deprecatedVersion?.id,
342
+ version: deprecatedVersion?.version,
343
+ status: deprecatedVersion?.status,
344
+ deprecatedAt: deprecatedVersion?.deprecatedAt,
345
+ deprecatedMessage: deprecatedVersion?.deprecatedMessage,
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Delete a version (yank)
351
+ */
352
+ export async function deleteVersion(
353
+ request: FastifyRequest<{ Params: SkillVersionParam }>,
354
+ reply: FastifyReply
355
+ ): Promise<void> {
356
+ const storage = await getStorage();
357
+ const { name, version: versionStr } = request.params;
358
+ const userId = request.user?.id;
359
+
360
+ // Parse name
361
+ const { scope, name: skillName } = parseSkillFullName(name);
362
+ const fullName = buildSkillFullName(skillName, scope);
363
+
364
+ const skill = await storage.getSkillByName(fullName);
365
+ if (!skill) {
366
+ throw new AppError(
367
+ ErrorCodes.SKILL_NOT_FOUND,
368
+ `Skill '${fullName}' not found`,
369
+ 404
370
+ );
371
+ }
372
+
373
+ // Check ownership
374
+ if (skill.ownerId && skill.ownerId !== userId && request.user?.type !== 'system') {
375
+ throw new AppError(
376
+ ErrorCodes.FORBIDDEN,
377
+ 'You do not have permission to delete versions for this skill',
378
+ 403
379
+ );
380
+ }
381
+
382
+ const version = await storage.getVersion(skill.id, versionStr);
383
+ if (!version) {
384
+ throw new AppError(
385
+ ErrorCodes.VERSION_NOT_FOUND,
386
+ `Version '${versionStr}' not found for skill '${fullName}'`,
387
+ 404
388
+ );
389
+ }
390
+
391
+ // Instead of hard delete, mark as yanked
392
+ await storage.updateVersion(version.id, {
393
+ status: 'yanked',
394
+ tag: undefined, // Remove any tag
395
+ });
396
+
397
+ // If this was the latest version, update skill's latestVersion
398
+ if (skill.latestVersion === versionStr) {
399
+ // Find next latest version
400
+ const versions = await storage.getVersions(skill.id, { limit: 10 });
401
+ const nextLatest = versions.items.find(v => v.id !== version.id && v.status === 'published');
402
+
403
+ if (nextLatest) {
404
+ await storage.updateSkill(skill.id, {
405
+ latestVersion: nextLatest.version,
406
+ latestVersionId: nextLatest.id,
407
+ });
408
+ await storage.updateVersion(nextLatest.id, { tag: 'latest' });
409
+ }
410
+ }
411
+
412
+ // Update version count
413
+ await storage.updateSkill(skill.id, {
414
+ stats: {
415
+ ...skill.stats,
416
+ versionCount: Math.max(0, skill.stats.versionCount - 1),
417
+ },
418
+ });
419
+
420
+ logger.info('Version yanked', {
421
+ skillId: skill.id,
422
+ fullName,
423
+ version: versionStr,
424
+ });
425
+
426
+ sendSuccess(request, reply, { yanked: true });
427
+ }
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Open Skills Hub - API Entry Point
4
+ */
5
+
6
+ import { createServer, startServer, stopServer } from './server.js';
7
+ import { initConfig, logger } from '@open-skills-hub/core';
8
+
9
+ // Export server utilities
10
+ export { createServer, startServer, stopServer } from './server.js';
11
+ export * from './middleware/index.js';
12
+ export * from './controllers/index.js';
13
+ export * from './routes/index.js';
14
+
15
+ /**
16
+ * Main entry point
17
+ */
18
+ async function main(): Promise<void> {
19
+ // Initialize configuration
20
+ const config = initConfig();
21
+ await config.load();
22
+
23
+ logger.info('Starting Open Skills Hub API', {
24
+ mode: config.get().mode,
25
+ port: config.get().server.port,
26
+ });
27
+
28
+ // Create and start server
29
+ const server = await createServer();
30
+
31
+ // Handle shutdown signals
32
+ const shutdown = async (signal: string) => {
33
+ logger.info(`Received ${signal}, shutting down...`);
34
+ try {
35
+ await stopServer(server);
36
+ process.exit(0);
37
+ } catch (error) {
38
+ logger.error('Error during shutdown', { error });
39
+ process.exit(1);
40
+ }
41
+ };
42
+
43
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
44
+ process.on('SIGINT', () => shutdown('SIGINT'));
45
+
46
+ // Handle uncaught errors
47
+ process.on('uncaughtException', (error) => {
48
+ logger.error('Uncaught exception', { error });
49
+ process.exit(1);
50
+ });
51
+
52
+ process.on('unhandledRejection', (reason) => {
53
+ logger.error('Unhandled rejection', { reason });
54
+ process.exit(1);
55
+ });
56
+
57
+ // Start the server
58
+ try {
59
+ const address = await startServer(server);
60
+ logger.info(`Open Skills Hub API started at ${address}`);
61
+
62
+ // Log available endpoints
63
+ logger.info('Available endpoints:', {
64
+ health: `${address}/health`,
65
+ ready: `${address}/ready`,
66
+ skills: `${address}/v1/skills`,
67
+ search: `${address}/v1/search`,
68
+ scan: `${address}/v1/scan`,
69
+ feedback: `${address}/v1/feedback`,
70
+ cache: `${address}/v1/cache`,
71
+ audit: `${address}/v1/audit`,
72
+ });
73
+ } catch (error) {
74
+ logger.error('Failed to start server', { error });
75
+ process.exit(1);
76
+ }
77
+ }
78
+
79
+ // Run if executed directly
80
+ const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
81
+ decodeURIComponent(import.meta.url) === `file://${process.argv[1]}`;
82
+ if (isMainModule) {
83
+ main().catch((error) => {
84
+ console.error('Fatal error:', error);
85
+ process.exit(1);
86
+ });
87
+ }