ts-packages 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.
@@ -0,0 +1,275 @@
1
+ import express from 'express';
2
+ import { ServerPlugin } from './types';
3
+
4
+ // Logging middleware
5
+ export function createLoggingMiddleware(format: 'simple' | 'detailed' = 'simple'): express.RequestHandler {
6
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
7
+ const start = Date.now();
8
+
9
+ res.on('finish', () => {
10
+ const duration = Date.now() - start;
11
+
12
+ if (format === 'detailed') {
13
+ console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms - ${req.ip}`);
14
+ } else {
15
+ console.log(`${req.method} ${req.url} - ${res.statusCode}`);
16
+ }
17
+ });
18
+
19
+ next();
20
+ };
21
+ }
22
+
23
+ // Error handling middleware
24
+ export function createErrorHandler(): express.ErrorRequestHandler {
25
+ return (err: unknown, req: express.Request, res: express.Response, next: express.NextFunction) => {
26
+ console.error('Error:', err);
27
+
28
+ if (res.headersSent) {
29
+ return next(err);
30
+ }
31
+
32
+ // Type guard for error objects
33
+ const errorObj = err as { status?: number; statusCode?: number; message?: string; stack?: string };
34
+
35
+ const status = errorObj.status || errorObj.statusCode || 500;
36
+ const message = process.env.NODE_ENV === 'production'
37
+ ? 'Internal Server Error'
38
+ : errorObj.message || 'Unknown error';
39
+
40
+ res.status(status).json({
41
+ status: false,
42
+ message,
43
+ ...(process.env.NODE_ENV !== 'production' && { stack: errorObj.stack })
44
+ });
45
+ };
46
+ }
47
+
48
+ // Request ID middleware
49
+ export function createRequestIdMiddleware(): express.RequestHandler {
50
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
51
+ const requestId = Math.random().toString(36).substring(2, 15);
52
+ (req as express.Request & { requestId: string }).requestId = requestId;
53
+ res.setHeader('X-Request-ID', requestId);
54
+ next();
55
+ };
56
+ }
57
+
58
+ // Validation middleware
59
+ export interface ValidationRule {
60
+ field: string;
61
+ required?: boolean;
62
+ type?: 'string' | 'number' | 'email' | 'boolean';
63
+ minLength?: number;
64
+ maxLength?: number;
65
+ pattern?: RegExp;
66
+ custom?: (value: unknown) => boolean | string;
67
+ }
68
+
69
+ export function createValidationMiddleware(rules: ValidationRule[]): express.RequestHandler {
70
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
71
+ const errors: string[] = [];
72
+
73
+ for (const rule of rules) {
74
+ const value = req.body[rule.field];
75
+
76
+ if (rule.required && (value === undefined || value === null || value === '')) {
77
+ errors.push(`${rule.field} is required`);
78
+ continue;
79
+ }
80
+
81
+ if (value === undefined || value === null) continue;
82
+
83
+ if (rule.type) {
84
+ switch (rule.type) {
85
+ case 'email':
86
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
87
+ if (!emailRegex.test(value)) {
88
+ errors.push(`${rule.field} must be a valid email`);
89
+ }
90
+ break;
91
+ case 'string':
92
+ if (typeof value !== 'string') {
93
+ errors.push(`${rule.field} must be a string`);
94
+ }
95
+ break;
96
+ case 'number':
97
+ if (typeof value !== 'number' && isNaN(Number(value))) {
98
+ errors.push(`${rule.field} must be a number`);
99
+ }
100
+ break;
101
+ case 'boolean':
102
+ if (typeof value !== 'boolean') {
103
+ errors.push(`${rule.field} must be a boolean`);
104
+ }
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (rule.minLength && value.length < rule.minLength) {
110
+ errors.push(`${rule.field} must be at least ${rule.minLength} characters`);
111
+ }
112
+ if (rule.maxLength && value.length > rule.maxLength) {
113
+ errors.push(`${rule.field} must be no more than ${rule.maxLength} characters`);
114
+ }
115
+
116
+ if (rule.pattern && !rule.pattern.test(value)) {
117
+ errors.push(`${rule.field} format is invalid`);
118
+ }
119
+
120
+ if (rule.custom) {
121
+ const result = rule.custom(value);
122
+ if (result !== true) {
123
+ errors.push(typeof result === 'string' ? result : `${rule.field} is invalid`);
124
+ }
125
+ }
126
+ }
127
+
128
+ if (errors.length > 0) {
129
+ return res.status(400).json({
130
+ status: false,
131
+ message: 'Validation failed',
132
+ errors
133
+ });
134
+ }
135
+
136
+ next();
137
+ };
138
+ }
139
+
140
+ // Rate limiting middleware
141
+ export interface RateLimitConfig {
142
+ windowMs?: number;
143
+ maxRequests?: number;
144
+ message?: string;
145
+ keyGenerator?: (req: express.Request) => string;
146
+ }
147
+
148
+ const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
149
+
150
+ export function createRateLimitMiddleware(config: RateLimitConfig = {}): express.RequestHandler {
151
+ const {
152
+ windowMs = 15 * 60 * 1000,
153
+ maxRequests = 100,
154
+ message = 'Too many requests, please try again later',
155
+ keyGenerator = (req) => req.ip || 'unknown'
156
+ } = config;
157
+
158
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
159
+ const key = keyGenerator(req);
160
+ const now = Date.now();
161
+ const record = rateLimitStore.get(key);
162
+
163
+ if (!record || now > record.resetTime) {
164
+ rateLimitStore.set(key, {
165
+ count: 1,
166
+ resetTime: now + windowMs
167
+ });
168
+ return next();
169
+ }
170
+
171
+ if (record.count >= maxRequests) {
172
+ return res.status(429).json({
173
+ status: false,
174
+ message,
175
+ retryAfter: Math.ceil((record.resetTime - now) / 1000)
176
+ });
177
+ }
178
+
179
+ record.count++;
180
+ next();
181
+ };
182
+ }
183
+
184
+ // Authentication middleware helper
185
+ export interface AuthConfig {
186
+ tokenExtractor?: (req: express.Request) => string | null;
187
+ tokenValidator?: (token: string) => Promise<unknown> | unknown;
188
+ unauthorizedMessage?: string;
189
+ }
190
+
191
+ export function createAuthMiddleware(config: AuthConfig): express.RequestHandler {
192
+ const {
193
+ tokenExtractor = (req) => {
194
+ const authHeader = req.headers.authorization;
195
+ if (authHeader && authHeader.startsWith('Bearer ')) {
196
+ return authHeader.substring(7);
197
+ }
198
+ return req.cookies?.token || null;
199
+ },
200
+ tokenValidator = () => { throw new Error('Token validator not implemented'); },
201
+ unauthorizedMessage = 'Unauthorized access'
202
+ } = config;
203
+
204
+ return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
205
+ try {
206
+ const token = tokenExtractor(req);
207
+
208
+ if (!token) {
209
+ return res.status(401).json({
210
+ status: false,
211
+ message: unauthorizedMessage
212
+ });
213
+ }
214
+
215
+ const user = await tokenValidator(token);
216
+ (req as express.Request & { user: unknown }).user = user;
217
+ next();
218
+ } catch (error) {
219
+ return res.status(401).json({
220
+ status: false,
221
+ message: unauthorizedMessage
222
+ });
223
+ }
224
+ };
225
+ }
226
+
227
+ // Plugin versions
228
+ export function withLogging(format: 'simple' | 'detailed' = 'simple'): ServerPlugin {
229
+ return (app: express.Application) => {
230
+ app.use(createLoggingMiddleware(format));
231
+ };
232
+ }
233
+
234
+ export function withErrorHandler(): ServerPlugin {
235
+ return (app: express.Application) => {
236
+ app.use(createErrorHandler());
237
+ };
238
+ }
239
+
240
+ export function withRequestId(): ServerPlugin {
241
+ return (app: express.Application) => {
242
+ app.use(createRequestIdMiddleware());
243
+ };
244
+ }
245
+
246
+ export function withValidation(rules: ValidationRule[]): ServerPlugin {
247
+ return (app: express.Application) => {
248
+ app.use(createValidationMiddleware(rules));
249
+ };
250
+ }
251
+
252
+ export function withRateLimit(config: RateLimitConfig = {}): ServerPlugin {
253
+ return (app: express.Application) => {
254
+ app.use(createRateLimitMiddleware(config));
255
+ };
256
+ }
257
+
258
+ export function withAuth(config: AuthConfig): ServerPlugin {
259
+ return (app: express.Application) => {
260
+ app.use(createAuthMiddleware(config));
261
+ };
262
+ }
263
+
264
+ // Convenience functions for route-specific middleware
265
+ export function validateFields(rules: ValidationRule[]): express.RequestHandler {
266
+ return createValidationMiddleware(rules);
267
+ }
268
+
269
+ export function rateLimit(config: RateLimitConfig = {}): express.RequestHandler {
270
+ return createRateLimitMiddleware(config);
271
+ }
272
+
273
+ export function requireAuth(config: AuthConfig): express.RequestHandler {
274
+ return createAuthMiddleware(config);
275
+ }
@@ -0,0 +1,320 @@
1
+ import express from 'express';
2
+ import { Server } from 'http';
3
+ import { ServerConfig, SocketIOConfig, SocketInstance } from './types';
4
+ import { createGracefulShutdown } from './shutdown';
5
+ import crypto from 'crypto';
6
+
7
+ export interface GrpcService {
8
+ service: Record<string, unknown>;
9
+ implementation: Record<string, (...args: unknown[]) => unknown>;
10
+ }
11
+
12
+ export interface RpcMethod {
13
+ [key: string]: (params: unknown[], callback: (error: Error | null, result?: unknown) => void) => void;
14
+ }
15
+
16
+ export interface WebhookConfig {
17
+ path: string;
18
+ secret?: string;
19
+ handler: (payload: Record<string, unknown>, headers: Record<string, string | string[]>) => void | Promise<void>;
20
+ }
21
+
22
+ export interface GrpcServerInstance {
23
+ start(): void;
24
+ forceShutdown(): void;
25
+ addService(service: unknown, implementation: unknown): void;
26
+ bindAsync(address: string, credentials: unknown, callback: () => void): void;
27
+ }
28
+
29
+
30
+
31
+ export interface ServerInstanceConfig extends Required<Omit<ServerConfig, 'socketIO' | 'name' | 'version'>> {
32
+ name: string;
33
+ version: string;
34
+ startTime: Date;
35
+ socketIO?: SocketIOConfig;
36
+ }
37
+
38
+ export interface ServerInstance {
39
+ app: express.Application;
40
+ server?: Server;
41
+ config: ServerInstanceConfig;
42
+ start(): Promise<ServerInstance>;
43
+ stop(): Promise<void>;
44
+ getInfo(): ServerInfo;
45
+
46
+ // Multi-protocol support
47
+ addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port?: number): void;
48
+ addRpcMethods(methods: RpcMethod, path?: string): void;
49
+ addWebhook(config: WebhookConfig): void;
50
+ addSocketIO(config?: SocketIOConfig): unknown;
51
+ }
52
+
53
+ export interface ServerInfo {
54
+ name: string;
55
+ version: string;
56
+ port: number;
57
+ uptime: number;
58
+ status: 'starting' | 'running' | 'stopping' | 'stopped';
59
+ startTime: Date;
60
+ }
61
+
62
+ export class ExpressServer implements ServerInstance {
63
+ public app: express.Application;
64
+ public server?: Server;
65
+ public config: ServerInstanceConfig;
66
+ private status: 'starting' | 'running' | 'stopping' | 'stopped' = 'stopped';
67
+ private grpcServices: GrpcService[] = [];
68
+ private grpcServer?: GrpcServerInstance;
69
+ private rpcMethods: RpcMethod = {};
70
+ private socketIO?: { close(): void };
71
+
72
+ constructor(
73
+ name: string = 'Express Server',
74
+ version: string = '1.0.0',
75
+ config: ServerConfig = {}
76
+ ) {
77
+ this.app = express();
78
+ this.config = {
79
+ name,
80
+ version,
81
+ startTime: new Date(),
82
+ port: config.port || 3000,
83
+ cors: config.cors ?? true,
84
+ helmet: config.helmet ?? true,
85
+ json: config.json ?? true,
86
+ customMiddleware: config.customMiddleware || [],
87
+ healthCheck: config.healthCheck ?? true,
88
+ gracefulShutdown: config.gracefulShutdown ?? true,
89
+ socketIO: config.socketIO
90
+ };
91
+ }
92
+
93
+ async start(): Promise<ServerInstance> {
94
+ this.status = 'starting';
95
+
96
+ return new Promise((resolve, reject) => {
97
+ try {
98
+ this.server = this.app.listen(this.config.port, () => {
99
+ this.status = 'running';
100
+ console.log(`🚀 ${this.config.name} v${this.config.version} running on http://localhost:${this.config.port}`);
101
+
102
+ if (this.config.gracefulShutdown) {
103
+ createGracefulShutdown(this.server!, {
104
+ onShutdown: async () => {
105
+ this.status = 'stopping';
106
+ }
107
+ });
108
+ }
109
+
110
+ resolve(this);
111
+ });
112
+
113
+ this.server.on('error', reject);
114
+ } catch (error: unknown) {
115
+ this.status = 'stopped';
116
+ reject(error);
117
+ }
118
+ });
119
+ }
120
+
121
+ async stop(): Promise<void> {
122
+ this.status = 'stopping';
123
+
124
+ // Stop gRPC server if running
125
+ if (this.grpcServer) {
126
+ this.grpcServer.forceShutdown();
127
+ }
128
+
129
+ // Stop Socket.IO server if running
130
+ if (this.socketIO) {
131
+ this.socketIO.close();
132
+ }
133
+
134
+ if (!this.server) {
135
+ this.status = 'stopped';
136
+ return;
137
+ }
138
+
139
+ return new Promise((resolve) => {
140
+ this.server!.close(() => {
141
+ this.status = 'stopped';
142
+ console.log(`👋 ${this.config.name} stopped`);
143
+ resolve();
144
+ });
145
+ });
146
+ }
147
+
148
+ getInfo(): ServerInfo {
149
+ return {
150
+ name: this.config.name,
151
+ version: this.config.version,
152
+ port: this.config.port,
153
+ uptime: Date.now() - this.config.startTime.getTime(),
154
+ status: this.status,
155
+ startTime: this.config.startTime
156
+ };
157
+ }
158
+
159
+ addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port: number = 50051): void {
160
+ this.grpcServices.push({ service, implementation });
161
+
162
+ // Lazy load gRPC to avoid dependency issues
163
+ if (!this.grpcServer) {
164
+ try {
165
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
166
+ const grpc = require('@grpc/grpc-js') as {
167
+ Server: new () => {
168
+ start(): void;
169
+ forceShutdown(): void;
170
+ addService(service: unknown, implementation: unknown): void;
171
+ bindAsync(address: string, credentials: unknown, callback: () => void): void;
172
+ };
173
+ ServerCredentials: { createInsecure(): unknown };
174
+ };
175
+ this.grpcServer = new grpc.Server();
176
+
177
+ // Add all services
178
+ this.grpcServices.forEach(({ service, implementation }) => {
179
+ this.grpcServer!.addService(service, implementation);
180
+ });
181
+
182
+ this.grpcServer.bindAsync(
183
+ `0.0.0.0:${port}`,
184
+ grpc.ServerCredentials.createInsecure(),
185
+ () => {
186
+ this.grpcServer!.start();
187
+ console.log(`🔗 gRPC server running on port ${port}`);
188
+ }
189
+ );
190
+ } catch (error: unknown) {
191
+ console.warn('gRPC not available. Install @grpc/grpc-js to use gRPC features.');
192
+ }
193
+ }
194
+ }
195
+
196
+ addRpcMethods(methods: RpcMethod, path: string = '/rpc'): void {
197
+ Object.assign(this.rpcMethods, methods);
198
+
199
+ try {
200
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
201
+ const jayson = require('jayson') as {
202
+ server: (methods: RpcMethod) => {
203
+ middleware(): express.RequestHandler;
204
+ };
205
+ };
206
+ const rpcServer = jayson.server(this.rpcMethods);
207
+ this.app.use(path, rpcServer.middleware());
208
+ console.log(`📡 JSON-RPC server mounted on ${path}`);
209
+ } catch (error: unknown) {
210
+ console.warn('JSON-RPC not available. Install jayson to use RPC features.');
211
+ }
212
+ }
213
+
214
+ addWebhook(config: WebhookConfig): void {
215
+ this.app.post(config.path, express.raw({ type: 'application/json' }), async (req, res) => {
216
+ try {
217
+ // Verify signature if secret provided
218
+ if (config.secret) {
219
+ const signature = req.headers['x-hub-signature-256'] || req.headers['x-signature-256'];
220
+ if (signature) {
221
+ const expectedSignature = crypto
222
+ .createHmac('sha256', config.secret)
223
+ .update(req.body)
224
+ .digest('hex');
225
+
226
+ const providedSignature = Array.isArray(signature) ? signature[0] : signature;
227
+ if (!providedSignature.includes(expectedSignature)) {
228
+ return res.status(401).json({ error: 'Invalid signature' });
229
+ }
230
+ }
231
+ }
232
+
233
+ // Parse JSON payload
234
+ const payload = JSON.parse(req.body.toString());
235
+
236
+ // Call handler
237
+ await config.handler(payload, req.headers as Record<string, string | string[]>);
238
+
239
+ res.status(200).json({ success: true });
240
+ } catch (error: unknown) {
241
+ console.error('Webhook error:', error);
242
+ res.status(500).json({ error: 'Webhook processing failed' });
243
+ }
244
+ });
245
+
246
+ console.log(`🪝 Webhook registered at ${config.path}`);
247
+ }
248
+
249
+ addSocketIO(config: SocketIOConfig = {}): unknown {
250
+ if (!this.server) {
251
+ throw new Error('Server must be started before adding Socket.IO');
252
+ }
253
+
254
+ try {
255
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
256
+ const { Server } = require('socket.io') as {
257
+ Server: new (server: Server, options?: {
258
+ cors?: {
259
+ origin?: string | string[] | boolean;
260
+ methods?: string[];
261
+ credentials?: boolean;
262
+ };
263
+ path?: string;
264
+ }) => {
265
+ on: (event: string, handler: (socket: unknown) => void) => void;
266
+ close: () => void;
267
+ };
268
+ };
269
+
270
+ // Configure CORS
271
+ const corsConfig = config.cors === true
272
+ ? { origin: '*', methods: ['GET', 'POST'] }
273
+ : config.cors || undefined;
274
+
275
+ // Create Socket.IO server
276
+ const io = new Server(this.server, {
277
+ cors: config.cors ? corsConfig : undefined,
278
+ path: config.path || '/socket.io'
279
+ });
280
+
281
+ // Store reference for cleanup
282
+ this.socketIO = io;
283
+
284
+ // Handle connections
285
+ io.on('connection', (socket: unknown) => {
286
+ const typedSocket = socket as SocketInstance;
287
+ console.log(`🔌 Socket connected: ${typedSocket.id}`);
288
+
289
+ // Call user-defined connection handler
290
+ if (config.onConnection) {
291
+ config.onConnection(socket);
292
+ }
293
+
294
+ // Handle disconnection
295
+ typedSocket.on('disconnect', (reason) => {
296
+ console.log(`🔌 Socket disconnected: ${typedSocket.id} - ${reason}`);
297
+
298
+ // Call user-defined disconnection handler
299
+ if (config.onDisconnection) {
300
+ config.onDisconnection(socket, reason as string);
301
+ }
302
+ });
303
+ });
304
+
305
+ console.log(`🔌 Socket.IO server attached${config.path ? ` at ${config.path}` : ''}`);
306
+ return io;
307
+ } catch (error: unknown) {
308
+ console.warn('Socket.IO not available. Install socket.io to use WebSocket features.');
309
+ return null;
310
+ }
311
+ }
312
+ }
313
+
314
+ export function createServer(
315
+ name?: string,
316
+ version?: string,
317
+ config?: ServerConfig
318
+ ): ServerInstance {
319
+ return new ExpressServer(name, version, config);
320
+ }
@@ -0,0 +1,60 @@
1
+ import { Server } from 'http';
2
+ import { GracefulShutdownConfig, ServerPlugin } from './types';
3
+
4
+ export function createGracefulShutdown(server: Server, config: GracefulShutdownConfig = {}): void {
5
+ const { timeout = 10000, onShutdown } = config;
6
+
7
+ const shutdown = async (signal: string) => {
8
+ console.log(`🛑 Received ${signal}, shutting down gracefully...`);
9
+
10
+ const shutdownTimer = setTimeout(() => {
11
+ console.log('⏰ Shutdown timeout reached, forcing exit');
12
+ process.exit(1);
13
+ }, timeout);
14
+
15
+ try {
16
+ // Run custom shutdown logic
17
+ if (onShutdown) {
18
+ await onShutdown();
19
+ }
20
+
21
+ // Close server
22
+ server.close(() => {
23
+ clearTimeout(shutdownTimer);
24
+ console.log('👋 Server closed. Exiting now.');
25
+ process.exit(0);
26
+ });
27
+ } catch (error) {
28
+ clearTimeout(shutdownTimer);
29
+ console.error('❌ Error during shutdown:', error);
30
+ process.exit(1);
31
+ }
32
+ };
33
+
34
+ process.on('SIGINT', () => shutdown('SIGINT'));
35
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
36
+ }
37
+
38
+ export function withGracefulShutdown(config: GracefulShutdownConfig = {}): ServerPlugin {
39
+ return (app, serverConfig) => {
40
+ // This plugin needs to be applied after server.listen()
41
+ // Store config for later use
42
+ (app as any).__gracefulShutdownConfig = config;
43
+ };
44
+ }
45
+
46
+ export function startServerWithShutdown(
47
+ app: any,
48
+ port: number,
49
+ shutdownConfig: GracefulShutdownConfig = {}
50
+ ): Server {
51
+ const server = app.listen(port, () => {
52
+ console.log(`🚀 Server running on http://localhost:${port}`);
53
+ });
54
+
55
+ // Apply graceful shutdown from stored config or provided config
56
+ const config = app.__gracefulShutdownConfig || shutdownConfig;
57
+ createGracefulShutdown(server, config);
58
+
59
+ return server;
60
+ }