@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,219 @@
1
+ /**
2
+ * Open Skills Hub - Authentication Middleware
3
+ */
4
+
5
+ import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
6
+ import * as crypto from 'crypto';
7
+ import {
8
+ getConfig,
9
+ AppError,
10
+ ErrorCodes,
11
+ } from '@open-skills-hub/core';
12
+ import { logger } from './logger.js';
13
+
14
+ export interface AuthUser {
15
+ id: string;
16
+ username?: string;
17
+ type: 'user' | 'api' | 'system';
18
+ permissions: string[];
19
+ }
20
+
21
+ // Extend FastifyRequest to include user
22
+ declare module 'fastify' {
23
+ interface FastifyRequest {
24
+ user?: AuthUser;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Extract Bearer token from Authorization header
30
+ */
31
+ function extractBearerToken(request: FastifyRequest): string | null {
32
+ const authHeader = request.headers.authorization;
33
+ if (!authHeader) return null;
34
+
35
+ const parts = authHeader.split(' ');
36
+ if (parts.length !== 2 || parts[0]?.toLowerCase() !== 'bearer') {
37
+ return null;
38
+ }
39
+
40
+ return parts[1] ?? null;
41
+ }
42
+
43
+ /**
44
+ * Verify JWT token (simplified - in production use jsonwebtoken library)
45
+ */
46
+ function verifyToken(token: string, secret: string): AuthUser | null {
47
+ try {
48
+ // Simple JWT verification (header.payload.signature)
49
+ const parts = token.split('.');
50
+ if (parts.length !== 3) return null;
51
+
52
+ const [headerB64, payloadB64, signatureB64] = parts;
53
+
54
+ // Verify signature
55
+ const data = `${headerB64}.${payloadB64}`;
56
+ const expectedSignature = crypto
57
+ .createHmac('sha256', secret)
58
+ .update(data)
59
+ .digest('base64url');
60
+
61
+ if (signatureB64 !== expectedSignature) {
62
+ return null;
63
+ }
64
+
65
+ // Decode payload
66
+ const payload = JSON.parse(Buffer.from(payloadB64!, 'base64url').toString());
67
+
68
+ // Check expiration
69
+ if (payload.exp && Date.now() / 1000 > payload.exp) {
70
+ return null;
71
+ }
72
+
73
+ return {
74
+ id: payload.sub,
75
+ username: payload.username,
76
+ type: payload.type || 'user',
77
+ permissions: payload.permissions || [],
78
+ };
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Authentication middleware - requires valid token
86
+ */
87
+ export async function requireAuth(
88
+ request: FastifyRequest,
89
+ reply: FastifyReply
90
+ ): Promise<void> {
91
+ const config = getConfig().get();
92
+
93
+ // Skip auth if not required
94
+ if (!config.auth.requireAuth) {
95
+ // Set a default system user for unauthenticated requests
96
+ request.user = {
97
+ id: 'anonymous',
98
+ type: 'user',
99
+ permissions: ['read'],
100
+ };
101
+ return;
102
+ }
103
+
104
+ const token = extractBearerToken(request);
105
+ if (!token) {
106
+ throw new AppError(
107
+ ErrorCodes.UNAUTHORIZED,
108
+ 'Authentication required',
109
+ 401
110
+ );
111
+ }
112
+
113
+ const secret = config.auth.jwtSecret;
114
+ if (!secret) {
115
+ logger.error('JWT secret not configured');
116
+ throw new AppError(
117
+ ErrorCodes.INTERNAL_ERROR,
118
+ 'Authentication not configured',
119
+ 500
120
+ );
121
+ }
122
+
123
+ const user = verifyToken(token, secret);
124
+ if (!user) {
125
+ throw new AppError(
126
+ ErrorCodes.TOKEN_INVALID,
127
+ 'Invalid or expired token',
128
+ 401
129
+ );
130
+ }
131
+
132
+ request.user = user;
133
+ }
134
+
135
+ /**
136
+ * Optional authentication - attaches user if token provided
137
+ */
138
+ export async function optionalAuth(
139
+ request: FastifyRequest,
140
+ _reply: FastifyReply
141
+ ): Promise<void> {
142
+ const config = getConfig().get();
143
+ const token = extractBearerToken(request);
144
+
145
+ if (!token) {
146
+ request.user = {
147
+ id: 'anonymous',
148
+ type: 'user',
149
+ permissions: ['read'],
150
+ };
151
+ return;
152
+ }
153
+
154
+ const secret = config.auth.jwtSecret;
155
+ if (!secret) {
156
+ request.user = {
157
+ id: 'anonymous',
158
+ type: 'user',
159
+ permissions: ['read'],
160
+ };
161
+ return;
162
+ }
163
+
164
+ const user = verifyToken(token, secret);
165
+ request.user = user ?? {
166
+ id: 'anonymous',
167
+ type: 'user',
168
+ permissions: ['read'],
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Permission check middleware
174
+ */
175
+ export function requirePermission(permission: string) {
176
+ return async function(
177
+ request: FastifyRequest,
178
+ _reply: FastifyReply
179
+ ): Promise<void> {
180
+ if (!request.user) {
181
+ throw new AppError(
182
+ ErrorCodes.UNAUTHORIZED,
183
+ 'Authentication required',
184
+ 401
185
+ );
186
+ }
187
+
188
+ if (!request.user.permissions.includes(permission) && !request.user.permissions.includes('admin')) {
189
+ throw new AppError(
190
+ ErrorCodes.INSUFFICIENT_PERMISSIONS,
191
+ `Permission '${permission}' required`,
192
+ 403
193
+ );
194
+ }
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Generate a simple JWT token (for testing)
200
+ */
201
+ export function generateToken(user: Omit<AuthUser, 'permissions'> & { permissions?: string[] }, secret: string, expiresIn: number = 86400): string {
202
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
203
+
204
+ const payload = Buffer.from(JSON.stringify({
205
+ sub: user.id,
206
+ username: user.username,
207
+ type: user.type,
208
+ permissions: user.permissions || [],
209
+ iat: Math.floor(Date.now() / 1000),
210
+ exp: Math.floor(Date.now() / 1000) + expiresIn,
211
+ })).toString('base64url');
212
+
213
+ const signature = crypto
214
+ .createHmac('sha256', secret)
215
+ .update(`${header}.${payload}`)
216
+ .digest('base64url');
217
+
218
+ return `${header}.${payload}.${signature}`;
219
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Open Skills Hub - Error Handling Middleware
3
+ */
4
+
5
+ import type { FastifyRequest, FastifyReply, FastifyError } from 'fastify';
6
+ import { ZodError } from 'zod';
7
+ import {
8
+ AppError,
9
+ ErrorCodes,
10
+ generateShortId,
11
+ now,
12
+ } from '@open-skills-hub/core';
13
+ import type { ApiResponse, ApiError } from '@open-skills-hub/core';
14
+ import { logger } from './logger.js';
15
+
16
+ /**
17
+ * Build standard API response
18
+ */
19
+ export function buildApiResponse<T>(
20
+ request: FastifyRequest,
21
+ data?: T,
22
+ error?: ApiError,
23
+ pagination?: ApiResponse['pagination']
24
+ ): ApiResponse<T> {
25
+ return {
26
+ success: !error,
27
+ data,
28
+ error,
29
+ pagination,
30
+ meta: {
31
+ requestId: request.id as string,
32
+ timestamp: now(),
33
+ },
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Send success response
39
+ */
40
+ export function sendSuccess<T>(
41
+ request: FastifyRequest,
42
+ reply: FastifyReply,
43
+ data: T,
44
+ statusCode: number = 200,
45
+ pagination?: ApiResponse['pagination']
46
+ ): void {
47
+ reply.status(statusCode).send(buildApiResponse(request, data, undefined, pagination));
48
+ }
49
+
50
+ /**
51
+ * Send error response
52
+ */
53
+ export function sendError(
54
+ request: FastifyRequest,
55
+ reply: FastifyReply,
56
+ code: string,
57
+ message: string,
58
+ statusCode: number = 500,
59
+ details?: Record<string, unknown>
60
+ ): void {
61
+ reply.status(statusCode).send(
62
+ buildApiResponse(request, undefined, { code, message, details })
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Convert Zod validation error to API error format
68
+ */
69
+ function formatZodError(error: ZodError): { message: string; details: Record<string, unknown> } {
70
+ const issues = error.issues.map(issue => ({
71
+ path: issue.path.join('.'),
72
+ message: issue.message,
73
+ code: issue.code,
74
+ }));
75
+
76
+ return {
77
+ message: 'Validation failed',
78
+ details: {
79
+ issues,
80
+ issueCount: issues.length,
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Global error handler
87
+ */
88
+ export function errorHandler(
89
+ error: FastifyError | Error,
90
+ request: FastifyRequest,
91
+ reply: FastifyReply
92
+ ): void {
93
+ // Log the error
94
+ logger.error('Request error', {
95
+ error: {
96
+ name: error.name,
97
+ message: error.message,
98
+ stack: error.stack,
99
+ },
100
+ requestId: request.id,
101
+ url: request.url,
102
+ method: request.method,
103
+ });
104
+
105
+ // Handle Zod validation errors
106
+ if (error instanceof ZodError) {
107
+ const { message, details } = formatZodError(error);
108
+ sendError(request, reply, ErrorCodes.VALIDATION_FAILED, message, 422, details);
109
+ return;
110
+ }
111
+
112
+ // Handle AppError
113
+ if (error instanceof AppError) {
114
+ sendError(
115
+ request,
116
+ reply,
117
+ error.code,
118
+ error.message,
119
+ error.statusCode,
120
+ error.details
121
+ );
122
+ return;
123
+ }
124
+
125
+ // Handle Fastify errors
126
+ if ('statusCode' in error) {
127
+ const fastifyError = error as FastifyError;
128
+
129
+ // Rate limit exceeded
130
+ if (fastifyError.statusCode === 429) {
131
+ sendError(
132
+ request,
133
+ reply,
134
+ ErrorCodes.RATE_LIMITED,
135
+ 'Too many requests, please try again later',
136
+ 429
137
+ );
138
+ return;
139
+ }
140
+
141
+ // Bad request
142
+ if (fastifyError.statusCode === 400) {
143
+ sendError(
144
+ request,
145
+ reply,
146
+ ErrorCodes.INVALID_REQUEST,
147
+ fastifyError.message || 'Bad request',
148
+ 400
149
+ );
150
+ return;
151
+ }
152
+ }
153
+
154
+ // Generic internal server error
155
+ sendError(
156
+ request,
157
+ reply,
158
+ ErrorCodes.INTERNAL_ERROR,
159
+ process.env['NODE_ENV'] === 'production'
160
+ ? 'An internal error occurred'
161
+ : error.message,
162
+ 500
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Not found handler
168
+ */
169
+ export function notFoundHandler(
170
+ request: FastifyRequest,
171
+ reply: FastifyReply
172
+ ): void {
173
+ sendError(
174
+ request,
175
+ reply,
176
+ 'NOT_FOUND',
177
+ `Route ${request.method} ${request.url} not found`,
178
+ 404
179
+ );
180
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Open Skills Hub - Middleware Exports
3
+ */
4
+
5
+ export * from './error.js';
6
+ export * from './logger.js';
7
+ export * from './auth.js';
8
+ export * from './validation.js';
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Open Skills Hub - Request Logger Middleware
3
+ */
4
+
5
+ import type { FastifyRequest, FastifyReply } from 'fastify';
6
+ import { logger as baseLogger } from '@open-skills-hub/core';
7
+ import pino from 'pino';
8
+
9
+ // Create API-specific logger
10
+ export const logger = baseLogger.child({ module: 'api' });
11
+
12
+ /**
13
+ * Request logging hook
14
+ */
15
+ export async function requestLogger(
16
+ request: FastifyRequest,
17
+ _reply: FastifyReply
18
+ ): Promise<void> {
19
+ // Skip health check logging to reduce noise
20
+ if (request.url === '/health' || request.url === '/ready') {
21
+ return;
22
+ }
23
+
24
+ logger.info('Incoming request', {
25
+ requestId: request.id,
26
+ method: request.method,
27
+ url: request.url,
28
+ query: request.query,
29
+ userAgent: request.headers['user-agent'],
30
+ ip: request.ip,
31
+ contentLength: request.headers['content-length'],
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Response logging hook
37
+ */
38
+ export async function responseLogger(
39
+ request: FastifyRequest,
40
+ reply: FastifyReply,
41
+ payload: unknown
42
+ ): Promise<unknown> {
43
+ // Skip health check logging
44
+ if (request.url === '/health' || request.url === '/ready') {
45
+ return payload;
46
+ }
47
+
48
+ const duration = request.startTime ? Date.now() - request.startTime : 0;
49
+
50
+ logger.info('Request completed', {
51
+ requestId: request.id,
52
+ method: request.method,
53
+ url: request.url,
54
+ statusCode: reply.statusCode,
55
+ duration: `${duration}ms`,
56
+ contentLength: reply.getHeader('content-length'),
57
+ });
58
+
59
+ return payload;
60
+ }
61
+
62
+ /**
63
+ * Create a child logger with request context
64
+ */
65
+ export function createRequestLogger(request: FastifyRequest): pino.Logger {
66
+ return logger.child({
67
+ requestId: request.id,
68
+ method: request.method,
69
+ url: request.url,
70
+ });
71
+ }