servcraft 0.1.0 → 0.1.1
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/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { logger } from '../../core/logger.js';
|
|
2
|
+
import type { SocketMiddleware, AuthenticatedSocket } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authentication Middleware
|
|
6
|
+
* Validates JWT token from socket handshake
|
|
7
|
+
*/
|
|
8
|
+
export function authMiddleware(): SocketMiddleware {
|
|
9
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
10
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Get token from auth or query
|
|
14
|
+
const token = authSocket.handshake.auth?.token || authSocket.handshake.query?.token;
|
|
15
|
+
|
|
16
|
+
if (!token) {
|
|
17
|
+
return next(new Error('Authentication token missing'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Verify token (mock - in production use JWT library)
|
|
21
|
+
// const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
22
|
+
// authSocket.userId = decoded.userId;
|
|
23
|
+
// authSocket.user = decoded;
|
|
24
|
+
|
|
25
|
+
// Mock authentication
|
|
26
|
+
authSocket.userId = 'user_123';
|
|
27
|
+
|
|
28
|
+
logger.debug({ socketId: authSocket.id, userId: authSocket.userId }, 'Socket authenticated');
|
|
29
|
+
|
|
30
|
+
next();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error({ error, socketId: authSocket.id }, 'Socket authentication failed');
|
|
33
|
+
next(new Error('Authentication failed'));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rate Limiting Middleware
|
|
40
|
+
* Limits socket connections per IP
|
|
41
|
+
*/
|
|
42
|
+
export function rateLimitMiddleware(maxConnections = 5, windowMs = 60000): SocketMiddleware {
|
|
43
|
+
const connections = new Map<string, { count: number; resetAt: number }>();
|
|
44
|
+
|
|
45
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
46
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
47
|
+
const ip =
|
|
48
|
+
authSocket.handshake.headers['x-forwarded-for'] ||
|
|
49
|
+
authSocket.handshake.headers['x-real-ip'] ||
|
|
50
|
+
'unknown';
|
|
51
|
+
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const key = ip as string;
|
|
54
|
+
|
|
55
|
+
// Clean up expired entries
|
|
56
|
+
for (const [k, v] of connections.entries()) {
|
|
57
|
+
if (v.resetAt < now) {
|
|
58
|
+
connections.delete(k);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check rate limit
|
|
63
|
+
const entry = connections.get(key);
|
|
64
|
+
|
|
65
|
+
if (entry && entry.count >= maxConnections) {
|
|
66
|
+
logger.warn({ ip, count: entry.count }, 'Socket rate limit exceeded');
|
|
67
|
+
return next(new Error('Too many connections'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Update count
|
|
71
|
+
if (entry) {
|
|
72
|
+
entry.count++;
|
|
73
|
+
} else {
|
|
74
|
+
connections.set(key, {
|
|
75
|
+
count: 1,
|
|
76
|
+
resetAt: now + windowMs,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cleanup on disconnect
|
|
81
|
+
authSocket.on('disconnect', () => {
|
|
82
|
+
const entry = connections.get(key);
|
|
83
|
+
if (entry) {
|
|
84
|
+
entry.count = Math.max(0, entry.count - 1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
next();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* CORS Middleware
|
|
94
|
+
* Validates origin
|
|
95
|
+
*/
|
|
96
|
+
export function corsMiddleware(allowedOrigins: string[]): SocketMiddleware {
|
|
97
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
98
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
99
|
+
const origin = authSocket.handshake.headers.origin;
|
|
100
|
+
|
|
101
|
+
if (!origin) {
|
|
102
|
+
return next(new Error('Origin not specified'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
|
|
106
|
+
next();
|
|
107
|
+
} else {
|
|
108
|
+
logger.warn({ origin, socketId: authSocket.id }, 'Origin not allowed');
|
|
109
|
+
next(new Error('Origin not allowed'));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Logging Middleware
|
|
116
|
+
* Logs all socket events
|
|
117
|
+
*/
|
|
118
|
+
export function loggingMiddleware(): SocketMiddleware {
|
|
119
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
120
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
121
|
+
|
|
122
|
+
logger.info(
|
|
123
|
+
{
|
|
124
|
+
socketId: authSocket.id,
|
|
125
|
+
userId: authSocket.userId,
|
|
126
|
+
ip: authSocket.handshake.headers['x-forwarded-for'] || 'unknown',
|
|
127
|
+
userAgent: authSocket.handshake.headers['user-agent'],
|
|
128
|
+
},
|
|
129
|
+
'Socket connecting'
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Log all events
|
|
133
|
+
const originalEmit = authSocket.emit;
|
|
134
|
+
authSocket.emit = function (event: string, ...args: unknown[]): boolean {
|
|
135
|
+
logger.debug({ socketId: authSocket.id, event, argsCount: args.length }, 'Socket emit');
|
|
136
|
+
return originalEmit.call(this, event, ...args);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
next();
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validation Middleware
|
|
145
|
+
* Validates socket handshake data
|
|
146
|
+
*/
|
|
147
|
+
export function validationMiddleware(requiredFields: string[]): SocketMiddleware {
|
|
148
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
149
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
150
|
+
const data = { ...authSocket.handshake.auth, ...authSocket.handshake.query };
|
|
151
|
+
|
|
152
|
+
for (const field of requiredFields) {
|
|
153
|
+
if (!data[field]) {
|
|
154
|
+
logger.warn({ socketId: authSocket.id, field }, 'Required field missing');
|
|
155
|
+
return next(new Error(`Required field missing: ${field}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
next();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Role-based Access Middleware
|
|
165
|
+
* Restricts access based on user roles
|
|
166
|
+
*/
|
|
167
|
+
export function roleMiddleware(allowedRoles: string[]): SocketMiddleware {
|
|
168
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
169
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
170
|
+
|
|
171
|
+
if (!authSocket.user) {
|
|
172
|
+
return next(new Error('User not authenticated'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const userRoles = authSocket.user.roles || [];
|
|
176
|
+
const hasRole = userRoles.some((role) => allowedRoles.includes(role));
|
|
177
|
+
|
|
178
|
+
if (!hasRole) {
|
|
179
|
+
logger.warn(
|
|
180
|
+
{
|
|
181
|
+
socketId: authSocket.id,
|
|
182
|
+
userId: authSocket.userId,
|
|
183
|
+
requiredRoles: allowedRoles,
|
|
184
|
+
userRoles,
|
|
185
|
+
},
|
|
186
|
+
'Insufficient permissions'
|
|
187
|
+
);
|
|
188
|
+
return next(new Error('Insufficient permissions'));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
next();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Namespace Middleware
|
|
197
|
+
* Validates namespace access
|
|
198
|
+
*/
|
|
199
|
+
export function namespaceMiddleware(
|
|
200
|
+
validateAccess: (userId: string, namespace: string) => Promise<boolean>
|
|
201
|
+
): SocketMiddleware {
|
|
202
|
+
return async (socket: unknown, next: (err?: Error) => void) => {
|
|
203
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
204
|
+
const namespace = authSocket.handshake.query.namespace as string;
|
|
205
|
+
|
|
206
|
+
if (!namespace) {
|
|
207
|
+
return next(new Error('Namespace not specified'));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!authSocket.userId) {
|
|
211
|
+
return next(new Error('User not authenticated'));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const hasAccess = await validateAccess(authSocket.userId, namespace);
|
|
216
|
+
|
|
217
|
+
if (!hasAccess) {
|
|
218
|
+
logger.warn(
|
|
219
|
+
{ socketId: authSocket.id, userId: authSocket.userId, namespace },
|
|
220
|
+
'Namespace access denied'
|
|
221
|
+
);
|
|
222
|
+
return next(new Error('Access to namespace denied'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
next();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.error(
|
|
228
|
+
{ error, socketId: authSocket.id, namespace },
|
|
229
|
+
'Error validating namespace access'
|
|
230
|
+
);
|
|
231
|
+
next(new Error('Failed to validate namespace access'));
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Throttling Middleware
|
|
238
|
+
* Limits event emission rate
|
|
239
|
+
*/
|
|
240
|
+
export function throttleMiddleware(maxEvents = 100, windowMs = 1000): SocketMiddleware {
|
|
241
|
+
const eventCounts = new Map<string, { count: number; resetAt: number }>();
|
|
242
|
+
|
|
243
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
244
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
245
|
+
|
|
246
|
+
// Wrap emit to count events
|
|
247
|
+
const originalEmit = authSocket.emit;
|
|
248
|
+
authSocket.emit = function (event: string, ...args: unknown[]): boolean {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
const key = authSocket.id;
|
|
251
|
+
|
|
252
|
+
const entry = eventCounts.get(key);
|
|
253
|
+
|
|
254
|
+
if (entry && entry.resetAt > now) {
|
|
255
|
+
if (entry.count >= maxEvents) {
|
|
256
|
+
logger.warn(
|
|
257
|
+
{ socketId: authSocket.id, event, count: entry.count },
|
|
258
|
+
'Event throttle limit exceeded'
|
|
259
|
+
);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
entry.count++;
|
|
263
|
+
} else {
|
|
264
|
+
eventCounts.set(key, {
|
|
265
|
+
count: 1,
|
|
266
|
+
resetAt: now + windowMs,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return originalEmit.call(this, event, ...args);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Cleanup on disconnect
|
|
274
|
+
authSocket.on('disconnect', () => {
|
|
275
|
+
eventCounts.delete(authSocket.id);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
next();
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Error Handling Middleware
|
|
284
|
+
* Catches and logs errors
|
|
285
|
+
*/
|
|
286
|
+
export function errorMiddleware(): SocketMiddleware {
|
|
287
|
+
return (socket: unknown, next: (err?: Error) => void) => {
|
|
288
|
+
const authSocket = socket as AuthenticatedSocket;
|
|
289
|
+
|
|
290
|
+
// Catch all errors
|
|
291
|
+
authSocket.on('error', (...args: unknown[]) => {
|
|
292
|
+
const error = args[0] instanceof Error ? args[0] : new Error(String(args[0]));
|
|
293
|
+
logger.error({ error, socketId: authSocket.id, userId: authSocket.userId }, 'Socket error');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
next();
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export interface WebSocketConfig {
|
|
2
|
+
/** CORS origin */
|
|
3
|
+
cors?: {
|
|
4
|
+
origin: string | string[];
|
|
5
|
+
credentials?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Path for socket.io */
|
|
8
|
+
path?: string;
|
|
9
|
+
/** Redis adapter for scaling */
|
|
10
|
+
redis?: {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
password?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Enable ping timeout */
|
|
16
|
+
pingTimeout?: number;
|
|
17
|
+
/** Ping interval */
|
|
18
|
+
pingInterval?: number;
|
|
19
|
+
/** Max payload size */
|
|
20
|
+
maxHttpBufferSize?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SocketUser {
|
|
24
|
+
id: string;
|
|
25
|
+
socketId: string;
|
|
26
|
+
username?: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
roles?: string[];
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
connectedAt: Date;
|
|
31
|
+
lastActivity: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Room {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
namespace: string;
|
|
38
|
+
members: Set<string>;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
createdBy?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Message {
|
|
45
|
+
id: string;
|
|
46
|
+
roomId: string;
|
|
47
|
+
userId: string;
|
|
48
|
+
username?: string;
|
|
49
|
+
content: string;
|
|
50
|
+
type: 'text' | 'image' | 'file' | 'system';
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
timestamp: Date;
|
|
53
|
+
edited?: boolean;
|
|
54
|
+
editedAt?: Date;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PresenceStatus {
|
|
58
|
+
userId: string;
|
|
59
|
+
status: 'online' | 'away' | 'busy' | 'offline';
|
|
60
|
+
lastSeen: Date;
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Notification {
|
|
65
|
+
id: string;
|
|
66
|
+
userId: string;
|
|
67
|
+
type: string;
|
|
68
|
+
title: string;
|
|
69
|
+
message: string;
|
|
70
|
+
data?: Record<string, unknown>;
|
|
71
|
+
read: boolean;
|
|
72
|
+
createdAt: Date;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TypingIndicator {
|
|
76
|
+
userId: string;
|
|
77
|
+
username?: string;
|
|
78
|
+
roomId: string;
|
|
79
|
+
timestamp: Date;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface WebSocketEvent {
|
|
83
|
+
event: string;
|
|
84
|
+
data: unknown;
|
|
85
|
+
timestamp: Date;
|
|
86
|
+
userId?: string;
|
|
87
|
+
socketId?: string;
|
|
88
|
+
roomId?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BroadcastOptions {
|
|
92
|
+
/** Target room */
|
|
93
|
+
room?: string;
|
|
94
|
+
/** Target namespace */
|
|
95
|
+
namespace?: string;
|
|
96
|
+
/** Exclude specific socket IDs */
|
|
97
|
+
except?: string[];
|
|
98
|
+
/** Only send to specific user IDs */
|
|
99
|
+
users?: string[];
|
|
100
|
+
/** Include metadata */
|
|
101
|
+
metadata?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ConnectionStats {
|
|
105
|
+
totalConnections: number;
|
|
106
|
+
activeConnections: number;
|
|
107
|
+
totalRooms: number;
|
|
108
|
+
messagesPerMinute: number;
|
|
109
|
+
bytesPerMinute: number;
|
|
110
|
+
avgLatency: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RoomStats {
|
|
114
|
+
roomId: string;
|
|
115
|
+
memberCount: number;
|
|
116
|
+
messageCount: number;
|
|
117
|
+
createdAt: Date;
|
|
118
|
+
lastActivity: Date;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Event types
|
|
122
|
+
export type SocketEventType =
|
|
123
|
+
| 'connection'
|
|
124
|
+
| 'disconnect'
|
|
125
|
+
| 'message'
|
|
126
|
+
| 'typing'
|
|
127
|
+
| 'presence'
|
|
128
|
+
| 'notification'
|
|
129
|
+
| 'room:join'
|
|
130
|
+
| 'room:leave'
|
|
131
|
+
| 'room:create'
|
|
132
|
+
| 'error';
|
|
133
|
+
|
|
134
|
+
// Socket.io event handlers
|
|
135
|
+
export interface SocketHandlers {
|
|
136
|
+
onConnection?: (socket: SocketUser) => void | Promise<void>;
|
|
137
|
+
onDisconnect?: (socket: SocketUser, reason: string) => void | Promise<void>;
|
|
138
|
+
onMessage?: (message: Message) => void | Promise<void>;
|
|
139
|
+
onError?: (error: Error, socket?: SocketUser) => void | Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Middleware types
|
|
143
|
+
export interface SocketMiddleware {
|
|
144
|
+
(socket: unknown, next: (err?: Error) => void): void | Promise<void>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface AuthenticatedSocket {
|
|
148
|
+
id: string;
|
|
149
|
+
userId?: string;
|
|
150
|
+
user?: SocketUser;
|
|
151
|
+
rooms: Set<string>;
|
|
152
|
+
handshake: {
|
|
153
|
+
auth: Record<string, unknown>;
|
|
154
|
+
headers: Record<string, string>;
|
|
155
|
+
query: Record<string, string>;
|
|
156
|
+
};
|
|
157
|
+
emit: (event: string, ...args: unknown[]) => boolean;
|
|
158
|
+
on: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
159
|
+
join: (room: string) => void;
|
|
160
|
+
leave: (room: string) => void;
|
|
161
|
+
disconnect: (close?: boolean) => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ChatMessage extends Message {
|
|
165
|
+
replyTo?: string;
|
|
166
|
+
mentions?: string[];
|
|
167
|
+
attachments?: Array<{
|
|
168
|
+
type: string;
|
|
169
|
+
url: string;
|
|
170
|
+
name: string;
|
|
171
|
+
size: number;
|
|
172
|
+
}>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface LiveEvent {
|
|
176
|
+
id: string;
|
|
177
|
+
type: string;
|
|
178
|
+
data: Record<string, unknown>;
|
|
179
|
+
timestamp: Date;
|
|
180
|
+
source?: string;
|
|
181
|
+
}
|