servcraft 0.1.0 → 0.1.3

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 (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Security Middleware
3
+ * Provides request-level security protections
4
+ */
5
+ import type {
6
+ FastifyRequest,
7
+ FastifyReply,
8
+ FastifyInstance,
9
+ HookHandlerDoneFunction,
10
+ } from 'fastify';
11
+ import { randomBytes } from 'crypto';
12
+ import {
13
+ sanitizeObject,
14
+ containsDangerousContent,
15
+ type SanitizeMiddlewareOptions,
16
+ } from './sanitize.js';
17
+ import { logger } from '../../core/logger.js';
18
+
19
+ // CSRF token storage (in production, use Redis)
20
+ const csrfTokens = new Map<string, { token: string; expires: number }>();
21
+ const CSRF_TOKEN_TTL = 3600000; // 1 hour
22
+
23
+ /**
24
+ * Input Sanitization Middleware
25
+ * Sanitizes request body, query, and params
26
+ */
27
+ export function sanitizeInput(
28
+ options: SanitizeMiddlewareOptions = {}
29
+ ): (request: FastifyRequest, _reply: FastifyReply) => Promise<void> {
30
+ return async (request: FastifyRequest, _reply: FastifyReply): Promise<void> => {
31
+ const { skipFields = [], onlyFields = [] } = options;
32
+
33
+ // Sanitize body
34
+ if (request.body && typeof request.body === 'object') {
35
+ const body = request.body as Record<string, unknown>;
36
+
37
+ // Check for dangerous content and log warning
38
+ const bodyStr = JSON.stringify(body);
39
+ if (containsDangerousContent(bodyStr)) {
40
+ logger.warn(
41
+ {
42
+ ip: request.ip,
43
+ path: request.url,
44
+ method: request.method,
45
+ },
46
+ 'Potentially dangerous content detected in request body'
47
+ );
48
+ }
49
+
50
+ // Apply field filters
51
+ if (onlyFields.length > 0) {
52
+ for (const field of onlyFields) {
53
+ if (body[field] && typeof body[field] === 'string') {
54
+ body[field] = sanitizeObject({ [field]: body[field] }, options)[field];
55
+ }
56
+ }
57
+ } else {
58
+ const filtered = { ...body };
59
+ for (const field of skipFields) {
60
+ delete filtered[field];
61
+ }
62
+ const sanitized = sanitizeObject(filtered, options);
63
+ for (const field of skipFields) {
64
+ sanitized[field] = body[field];
65
+ }
66
+ request.body = sanitized;
67
+ }
68
+ }
69
+
70
+ // Sanitize query params
71
+ if (request.query && typeof request.query === 'object') {
72
+ request.query = sanitizeObject(request.query as Record<string, unknown>, options);
73
+ }
74
+
75
+ // Sanitize URL params
76
+ if (request.params && typeof request.params === 'object') {
77
+ request.params = sanitizeObject(request.params as Record<string, unknown>, options);
78
+ }
79
+ };
80
+ }
81
+
82
+ /**
83
+ * CSRF Protection Middleware
84
+ * Validates CSRF tokens for state-changing requests
85
+ */
86
+ export function csrfProtection(): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
87
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
88
+ // Skip for safe methods
89
+ if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
90
+ return;
91
+ }
92
+
93
+ // Skip for API requests with valid JWT (they have their own protection)
94
+ const authHeader = request.headers.authorization;
95
+ if (authHeader?.startsWith('Bearer ')) {
96
+ return;
97
+ }
98
+
99
+ // Get CSRF token from header or body
100
+ const tokenFromHeader = request.headers['x-csrf-token'] as string;
101
+ const tokenFromBody = (request.body as Record<string, unknown>)?._csrf as string;
102
+ const token = tokenFromHeader || tokenFromBody;
103
+
104
+ // Get session ID (from cookie or header)
105
+ const sessionId = (request.headers['x-session-id'] as string) || request.ip;
106
+
107
+ if (!token) {
108
+ logger.warn({ ip: request.ip, path: request.url }, 'CSRF token missing');
109
+ return reply.status(403).send({
110
+ success: false,
111
+ message: 'CSRF token missing',
112
+ code: 'CSRF_TOKEN_MISSING',
113
+ });
114
+ }
115
+
116
+ // Validate token
117
+ const storedToken = csrfTokens.get(sessionId);
118
+ if (!storedToken || storedToken.token !== token || storedToken.expires < Date.now()) {
119
+ logger.warn({ ip: request.ip, path: request.url }, 'Invalid CSRF token');
120
+ return reply.status(403).send({
121
+ success: false,
122
+ message: 'Invalid CSRF token',
123
+ code: 'CSRF_TOKEN_INVALID',
124
+ });
125
+ }
126
+
127
+ // Token is valid, generate a new one for next request (token rotation)
128
+ const newToken = generateCsrfToken(sessionId);
129
+ reply.header('X-CSRF-Token', newToken);
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Generate a new CSRF token
135
+ */
136
+ export function generateCsrfToken(sessionId: string): string {
137
+ const token = randomBytes(32).toString('hex');
138
+ csrfTokens.set(sessionId, {
139
+ token,
140
+ expires: Date.now() + CSRF_TOKEN_TTL,
141
+ });
142
+
143
+ // Cleanup expired tokens periodically
144
+ if (csrfTokens.size > 10000) {
145
+ cleanupExpiredCsrfTokens();
146
+ }
147
+
148
+ return token;
149
+ }
150
+
151
+ /**
152
+ * Cleanup expired CSRF tokens
153
+ */
154
+ function cleanupExpiredCsrfTokens(): void {
155
+ const now = Date.now();
156
+ for (const [key, value] of csrfTokens.entries()) {
157
+ if (value.expires < now) {
158
+ csrfTokens.delete(key);
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * HTTP Parameter Pollution Protection
165
+ * Ensures query parameters are not arrays when they shouldn't be
166
+ */
167
+ export function hppProtection(
168
+ allowedArrayParams: string[] = []
169
+ ): (request: FastifyRequest, _reply: FastifyReply) => Promise<void> {
170
+ return async (request: FastifyRequest, _reply: FastifyReply): Promise<void> => {
171
+ if (request.query && typeof request.query === 'object') {
172
+ const query = request.query as Record<string, unknown>;
173
+
174
+ for (const [key, value] of Object.entries(query)) {
175
+ if (Array.isArray(value) && !allowedArrayParams.includes(key)) {
176
+ // Take the last value (most common behavior)
177
+ query[key] = value[value.length - 1];
178
+ }
179
+ }
180
+ }
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Security Headers Middleware
186
+ * Adds additional security headers beyond Helmet
187
+ */
188
+ export function securityHeaders(): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
189
+ return async (_request: FastifyRequest, reply: FastifyReply): Promise<void> => {
190
+ // Prevent browsers from MIME-sniffing
191
+ reply.header('X-Content-Type-Options', 'nosniff');
192
+
193
+ // Prevent clickjacking
194
+ reply.header('X-Frame-Options', 'DENY');
195
+
196
+ // Enable XSS filter in browsers
197
+ reply.header('X-XSS-Protection', '1; mode=block');
198
+
199
+ // Control referrer information
200
+ reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');
201
+
202
+ // Permissions Policy (formerly Feature-Policy)
203
+ reply.header(
204
+ 'Permissions-Policy',
205
+ 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
206
+ );
207
+
208
+ // Prevent caching of sensitive data
209
+ reply.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
210
+ reply.header('Pragma', 'no-cache');
211
+ reply.header('Expires', '0');
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Request Size Limit Middleware
217
+ * Additional protection against large payload attacks
218
+ */
219
+ export function requestSizeLimit(
220
+ maxSizeBytes: number = 10 * 1024 * 1024
221
+ ): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
222
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
223
+ const contentLength = parseInt(request.headers['content-length'] || '0', 10);
224
+
225
+ if (contentLength > maxSizeBytes) {
226
+ logger.warn(
227
+ {
228
+ ip: request.ip,
229
+ contentLength,
230
+ maxSize: maxSizeBytes,
231
+ },
232
+ 'Request payload too large'
233
+ );
234
+
235
+ return reply.status(413).send({
236
+ success: false,
237
+ message: 'Payload too large',
238
+ code: 'PAYLOAD_TOO_LARGE',
239
+ });
240
+ }
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Suspicious Activity Detection
246
+ * Logs and optionally blocks suspicious patterns
247
+ */
248
+ export function suspiciousActivityDetection(
249
+ options: { blockSuspicious?: boolean } = {}
250
+ ): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
251
+ const { blockSuspicious = false } = options;
252
+
253
+ // Suspicious patterns
254
+ const suspiciousPatterns = [
255
+ /\.\.\//g, // Path traversal
256
+ /<script/gi, // Script injection
257
+ /union\s+select/gi, // SQL injection
258
+ /\$\{.*\}/g, // Template injection
259
+ /{{.*}}/g, // Template injection
260
+ /\bexec\s*\(/gi, // Code execution
261
+ /\beval\s*\(/gi, // Code execution
262
+ ];
263
+
264
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
265
+ const requestString = JSON.stringify({
266
+ url: request.url,
267
+ query: request.query,
268
+ body: request.body,
269
+ params: request.params,
270
+ });
271
+
272
+ for (const pattern of suspiciousPatterns) {
273
+ if (pattern.test(requestString)) {
274
+ logger.warn(
275
+ {
276
+ ip: request.ip,
277
+ path: request.url,
278
+ method: request.method,
279
+ pattern: pattern.source,
280
+ },
281
+ 'Suspicious activity detected'
282
+ );
283
+
284
+ if (blockSuspicious) {
285
+ return reply.status(400).send({
286
+ success: false,
287
+ message: 'Request blocked due to suspicious content',
288
+ code: 'SUSPICIOUS_REQUEST',
289
+ });
290
+ }
291
+
292
+ break;
293
+ }
294
+ }
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Register all security middlewares
300
+ */
301
+ export async function registerSecurityMiddlewares(
302
+ app: FastifyInstance,
303
+ options: {
304
+ sanitize?: boolean | SanitizeMiddlewareOptions;
305
+ csrf?: boolean;
306
+ hpp?: boolean | string[];
307
+ headers?: boolean;
308
+ sizeLimit?: boolean | number;
309
+ suspicionDetection?: boolean | { blockSuspicious: boolean };
310
+ } = {}
311
+ ): Promise<void> {
312
+ const {
313
+ sanitize = true,
314
+ csrf = false, // Disabled by default for API-first apps
315
+ hpp = true,
316
+ headers = true,
317
+ sizeLimit = true,
318
+ suspicionDetection = true,
319
+ } = options;
320
+
321
+ // Security headers (run first)
322
+ if (headers) {
323
+ app.addHook(
324
+ 'onRequest',
325
+ securityHeaders() as (
326
+ request: FastifyRequest,
327
+ reply: FastifyReply,
328
+ done: HookHandlerDoneFunction
329
+ ) => void
330
+ );
331
+ }
332
+
333
+ // Request size limit
334
+ if (sizeLimit) {
335
+ const maxSize = typeof sizeLimit === 'number' ? sizeLimit : 10 * 1024 * 1024;
336
+ app.addHook(
337
+ 'preHandler',
338
+ requestSizeLimit(maxSize) as (
339
+ request: FastifyRequest,
340
+ reply: FastifyReply,
341
+ done: HookHandlerDoneFunction
342
+ ) => void
343
+ );
344
+ }
345
+
346
+ // HPP protection
347
+ if (hpp) {
348
+ const allowedArrays = Array.isArray(hpp) ? hpp : [];
349
+ app.addHook(
350
+ 'preHandler',
351
+ hppProtection(allowedArrays) as (
352
+ request: FastifyRequest,
353
+ reply: FastifyReply,
354
+ done: HookHandlerDoneFunction
355
+ ) => void
356
+ );
357
+ }
358
+
359
+ // Input sanitization
360
+ if (sanitize) {
361
+ const sanitizeOpts = typeof sanitize === 'object' ? sanitize : {};
362
+ app.addHook(
363
+ 'preHandler',
364
+ sanitizeInput(sanitizeOpts) as (
365
+ request: FastifyRequest,
366
+ reply: FastifyReply,
367
+ done: HookHandlerDoneFunction
368
+ ) => void
369
+ );
370
+ }
371
+
372
+ // Suspicious activity detection
373
+ if (suspicionDetection) {
374
+ const detectionOpts = typeof suspicionDetection === 'object' ? suspicionDetection : {};
375
+ app.addHook(
376
+ 'preHandler',
377
+ suspiciousActivityDetection(detectionOpts) as (
378
+ request: FastifyRequest,
379
+ reply: FastifyReply,
380
+ done: HookHandlerDoneFunction
381
+ ) => void
382
+ );
383
+ }
384
+
385
+ // CSRF protection (only for web apps, not APIs)
386
+ if (csrf) {
387
+ app.addHook(
388
+ 'preHandler',
389
+ csrfProtection() as (
390
+ request: FastifyRequest,
391
+ reply: FastifyReply,
392
+ done: HookHandlerDoneFunction
393
+ ) => void
394
+ );
395
+ }
396
+
397
+ logger.info('Security middlewares registered');
398
+ }
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './session.service.js';
3
+ export * from './session.repository.js';
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Session Repository
3
+ * Prisma-based persistence for sessions (optional backup storage)
4
+ */
5
+ import type { PrismaClient, Session as PrismaSession } from '@prisma/client';
6
+ import type { Session, CreateSessionData } from './types.js';
7
+
8
+ export class SessionRepository {
9
+ constructor(private prisma: PrismaClient) {}
10
+
11
+ /**
12
+ * Create a new session in database
13
+ */
14
+ async create(data: CreateSessionData & { id: string; expiresAt: Date }): Promise<Session> {
15
+ const session = await this.prisma.session.create({
16
+ data: {
17
+ id: data.id,
18
+ userId: data.userId,
19
+ userAgent: data.userAgent,
20
+ ipAddress: data.ipAddress,
21
+ expiresAt: data.expiresAt,
22
+ },
23
+ });
24
+
25
+ return this.mapFromPrisma(session);
26
+ }
27
+
28
+ /**
29
+ * Find session by ID
30
+ */
31
+ async findById(id: string): Promise<Session | null> {
32
+ const session = await this.prisma.session.findUnique({
33
+ where: { id },
34
+ });
35
+
36
+ if (!session) return null;
37
+
38
+ // Check if expired
39
+ if (session.expiresAt < new Date()) {
40
+ await this.delete(id);
41
+ return null;
42
+ }
43
+
44
+ return this.mapFromPrisma(session);
45
+ }
46
+
47
+ /**
48
+ * Find all sessions for a user
49
+ */
50
+ async findByUserId(userId: string): Promise<Session[]> {
51
+ const sessions = await this.prisma.session.findMany({
52
+ where: {
53
+ userId,
54
+ expiresAt: { gt: new Date() },
55
+ },
56
+ orderBy: { createdAt: 'desc' },
57
+ });
58
+
59
+ return sessions.map((s) => this.mapFromPrisma(s));
60
+ }
61
+
62
+ /**
63
+ * Update session expiration
64
+ */
65
+ async updateExpiration(id: string, expiresAt: Date): Promise<Session | null> {
66
+ try {
67
+ const session = await this.prisma.session.update({
68
+ where: { id },
69
+ data: { expiresAt },
70
+ });
71
+
72
+ return this.mapFromPrisma(session);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Delete session by ID
80
+ */
81
+ async delete(id: string): Promise<boolean> {
82
+ try {
83
+ await this.prisma.session.delete({
84
+ where: { id },
85
+ });
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Delete all sessions for a user
94
+ */
95
+ async deleteByUserId(userId: string): Promise<number> {
96
+ const result = await this.prisma.session.deleteMany({
97
+ where: { userId },
98
+ });
99
+
100
+ return result.count;
101
+ }
102
+
103
+ /**
104
+ * Delete expired sessions
105
+ */
106
+ async deleteExpired(): Promise<number> {
107
+ const result = await this.prisma.session.deleteMany({
108
+ where: {
109
+ expiresAt: { lt: new Date() },
110
+ },
111
+ });
112
+
113
+ return result.count;
114
+ }
115
+
116
+ /**
117
+ * Count active sessions
118
+ */
119
+ async countActive(): Promise<number> {
120
+ return this.prisma.session.count({
121
+ where: {
122
+ expiresAt: { gt: new Date() },
123
+ },
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Count sessions per user
129
+ */
130
+ async countByUser(userId: string): Promise<number> {
131
+ return this.prisma.session.count({
132
+ where: {
133
+ userId,
134
+ expiresAt: { gt: new Date() },
135
+ },
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Clear all sessions (for testing)
141
+ */
142
+ async clear(): Promise<void> {
143
+ await this.prisma.session.deleteMany();
144
+ }
145
+
146
+ /**
147
+ * Map Prisma model to domain type
148
+ */
149
+ private mapFromPrisma(session: PrismaSession): Session {
150
+ return {
151
+ id: session.id,
152
+ userId: session.userId,
153
+ userAgent: session.userAgent ?? undefined,
154
+ ipAddress: session.ipAddress ?? undefined,
155
+ expiresAt: session.expiresAt,
156
+ createdAt: session.createdAt,
157
+ };
158
+ }
159
+ }