@naman_deep_singh/server-utils 1.0.7 → 1.1.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.
- package/README.md +147 -7
- package/dist/{index.d.ts → cjs/index.d.ts} +2 -3
- package/dist/{index.js → cjs/index.js} +1 -3
- package/dist/{middleware.js → cjs/middleware.js} +37 -10
- package/dist/{periodic-health.d.ts → cjs/periodic-health.d.ts} +0 -1
- package/dist/{periodic-health.js → cjs/periodic-health.js} +0 -4
- package/dist/{server.js → cjs/server.js} +1 -1
- package/dist/{utils.js → cjs/utils.js} +14 -8
- package/dist/esm/health.d.ts +5 -0
- package/dist/esm/health.js +40 -0
- package/dist/esm/index.d.ts +46 -0
- package/dist/esm/index.js +58 -0
- package/dist/esm/middleware.d.ts +37 -0
- package/dist/esm/middleware.js +229 -0
- package/dist/esm/periodic-health.d.ts +11 -0
- package/dist/esm/periodic-health.js +64 -0
- package/dist/esm/server.d.ts +69 -0
- package/dist/esm/server.js +271 -0
- package/dist/esm/shutdown.d.ts +5 -0
- package/dist/esm/shutdown.js +52 -0
- package/dist/esm/types.d.ts +70 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +38 -0
- package/dist/types/health.d.ts +5 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/types/middleware.d.ts +37 -0
- package/dist/types/periodic-health.d.ts +11 -0
- package/dist/types/server.d.ts +69 -0
- package/dist/types/shutdown.d.ts +5 -0
- package/dist/types/types.d.ts +70 -0
- package/dist/types/utils.d.ts +3 -0
- package/package.json +22 -7
- package/src/health.ts +0 -47
- package/src/index.ts +0 -127
- package/src/middleware.ts +0 -275
- package/src/periodic-health.ts +0 -87
- package/src/server.ts +0 -412
- package/src/shutdown.ts +0 -69
- package/src/types.ts +0 -80
- package/src/utils.ts +0 -34
- package/tsconfig.json +0 -21
- /package/dist/{health.d.ts → cjs/health.d.ts} +0 -0
- /package/dist/{health.js → cjs/health.js} +0 -0
- /package/dist/{middleware.d.ts → cjs/middleware.d.ts} +0 -0
- /package/dist/{server.d.ts → cjs/server.d.ts} +0 -0
- /package/dist/{shutdown.d.ts → cjs/shutdown.d.ts} +0 -0
- /package/dist/{shutdown.js → cjs/shutdown.js} +0 -0
- /package/dist/{types.d.ts → cjs/types.d.ts} +0 -0
- /package/dist/{types.js → cjs/types.js} +0 -0
- /package/dist/{utils.d.ts → cjs/utils.d.ts} +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Logging middleware
|
|
2
|
+
export function createLoggingMiddleware(format = 'simple') {
|
|
3
|
+
return (req, res, next) => {
|
|
4
|
+
const start = Date.now();
|
|
5
|
+
res.on('finish', () => {
|
|
6
|
+
const duration = Date.now() - start;
|
|
7
|
+
if (format === 'detailed') {
|
|
8
|
+
console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms - ${req.ip}`);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
console.log(`${req.method} ${req.url} - ${res.statusCode}`);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
next();
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Error handling middleware
|
|
18
|
+
export function createErrorHandler() {
|
|
19
|
+
return (err, req, res, next) => {
|
|
20
|
+
console.error('Error:', err);
|
|
21
|
+
if (res.headersSent) {
|
|
22
|
+
return next(err);
|
|
23
|
+
}
|
|
24
|
+
// Type guard for error objects
|
|
25
|
+
const errorObj = err;
|
|
26
|
+
const status = errorObj.status || errorObj.statusCode || 500;
|
|
27
|
+
const message = process.env.NODE_ENV === 'production'
|
|
28
|
+
? 'Internal Server Error'
|
|
29
|
+
: errorObj.message || 'Unknown error';
|
|
30
|
+
res.status(status).json({
|
|
31
|
+
success: false,
|
|
32
|
+
message,
|
|
33
|
+
data: undefined,
|
|
34
|
+
error: {
|
|
35
|
+
message,
|
|
36
|
+
...(process.env.NODE_ENV !== 'production' && { details: { stack: errorObj.stack } })
|
|
37
|
+
},
|
|
38
|
+
meta: null
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Request ID middleware
|
|
43
|
+
export function createRequestIdMiddleware() {
|
|
44
|
+
return (req, res, next) => {
|
|
45
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
46
|
+
req.requestId = requestId;
|
|
47
|
+
res.setHeader('X-Request-ID', requestId);
|
|
48
|
+
next();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createValidationMiddleware(rules) {
|
|
52
|
+
return (req, res, next) => {
|
|
53
|
+
const errors = [];
|
|
54
|
+
for (const rule of rules) {
|
|
55
|
+
const value = req.body[rule.field];
|
|
56
|
+
if (rule.required && (value === undefined || value === null || value === '')) {
|
|
57
|
+
errors.push(`${rule.field} is required`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (value === undefined || value === null)
|
|
61
|
+
continue;
|
|
62
|
+
if (rule.type) {
|
|
63
|
+
switch (rule.type) {
|
|
64
|
+
case 'email':
|
|
65
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
66
|
+
if (!emailRegex.test(value)) {
|
|
67
|
+
errors.push(`${rule.field} must be a valid email`);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case 'string':
|
|
71
|
+
if (typeof value !== 'string') {
|
|
72
|
+
errors.push(`${rule.field} must be a string`);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'number':
|
|
76
|
+
if (typeof value !== 'number' && isNaN(Number(value))) {
|
|
77
|
+
errors.push(`${rule.field} must be a number`);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case 'boolean':
|
|
81
|
+
if (typeof value !== 'boolean') {
|
|
82
|
+
errors.push(`${rule.field} must be a boolean`);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (rule.minLength && value.length < rule.minLength) {
|
|
88
|
+
errors.push(`${rule.field} must be at least ${rule.minLength} characters`);
|
|
89
|
+
}
|
|
90
|
+
if (rule.maxLength && value.length > rule.maxLength) {
|
|
91
|
+
errors.push(`${rule.field} must be no more than ${rule.maxLength} characters`);
|
|
92
|
+
}
|
|
93
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
94
|
+
errors.push(`${rule.field} format is invalid`);
|
|
95
|
+
}
|
|
96
|
+
if (rule.custom) {
|
|
97
|
+
const result = rule.custom(value);
|
|
98
|
+
if (result !== true) {
|
|
99
|
+
errors.push(typeof result === 'string' ? result : `${rule.field} is invalid`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (errors.length > 0) {
|
|
104
|
+
return res.status(400).json({
|
|
105
|
+
success: false,
|
|
106
|
+
message: 'Validation failed',
|
|
107
|
+
data: undefined,
|
|
108
|
+
error: {
|
|
109
|
+
message: 'Validation failed',
|
|
110
|
+
details: errors
|
|
111
|
+
},
|
|
112
|
+
meta: null
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const rateLimitStore = new Map();
|
|
119
|
+
export function createRateLimitMiddleware(config = {}) {
|
|
120
|
+
const { windowMs = 15 * 60 * 1000, maxRequests = 100, message = 'Too many requests, please try again later', keyGenerator = (req) => req.ip || 'unknown' } = config;
|
|
121
|
+
return (req, res, next) => {
|
|
122
|
+
const key = keyGenerator(req);
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const record = rateLimitStore.get(key);
|
|
125
|
+
if (!record || now > record.resetTime) {
|
|
126
|
+
rateLimitStore.set(key, {
|
|
127
|
+
count: 1,
|
|
128
|
+
resetTime: now + windowMs
|
|
129
|
+
});
|
|
130
|
+
return next();
|
|
131
|
+
}
|
|
132
|
+
if (record.count >= maxRequests) {
|
|
133
|
+
return res.status(429).json({
|
|
134
|
+
success: false,
|
|
135
|
+
message,
|
|
136
|
+
data: undefined,
|
|
137
|
+
error: {
|
|
138
|
+
message,
|
|
139
|
+
details: {
|
|
140
|
+
retryAfter: Math.ceil((record.resetTime - now) / 1000)
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
meta: null
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
record.count++;
|
|
147
|
+
next();
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function createAuthMiddleware(config) {
|
|
151
|
+
const { tokenExtractor = (req) => {
|
|
152
|
+
const authHeader = req.headers.authorization;
|
|
153
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
154
|
+
return authHeader.substring(7);
|
|
155
|
+
}
|
|
156
|
+
return req.cookies?.token || null;
|
|
157
|
+
}, tokenValidator = () => { throw new Error('Token validator not implemented'); }, unauthorizedMessage = 'Unauthorized access' } = config;
|
|
158
|
+
return async (req, res, next) => {
|
|
159
|
+
try {
|
|
160
|
+
const token = tokenExtractor(req);
|
|
161
|
+
if (!token) {
|
|
162
|
+
return res.status(401).json({
|
|
163
|
+
success: false,
|
|
164
|
+
message: unauthorizedMessage,
|
|
165
|
+
data: undefined,
|
|
166
|
+
error: {
|
|
167
|
+
message: unauthorizedMessage
|
|
168
|
+
},
|
|
169
|
+
meta: null
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const user = await tokenValidator(token);
|
|
173
|
+
req.user = user;
|
|
174
|
+
next();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
return res.status(401).json({
|
|
178
|
+
success: false,
|
|
179
|
+
message: unauthorizedMessage,
|
|
180
|
+
data: undefined,
|
|
181
|
+
error: {
|
|
182
|
+
message: unauthorizedMessage
|
|
183
|
+
},
|
|
184
|
+
meta: null
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Plugin versions
|
|
190
|
+
export function withLogging(format = 'simple') {
|
|
191
|
+
return (app) => {
|
|
192
|
+
app.use(createLoggingMiddleware(format));
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export function withErrorHandler() {
|
|
196
|
+
return (app) => {
|
|
197
|
+
app.use(createErrorHandler());
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function withRequestId() {
|
|
201
|
+
return (app) => {
|
|
202
|
+
app.use(createRequestIdMiddleware());
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export function withValidation(rules) {
|
|
206
|
+
return (app) => {
|
|
207
|
+
app.use(createValidationMiddleware(rules));
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function withRateLimit(config = {}) {
|
|
211
|
+
return (app) => {
|
|
212
|
+
app.use(createRateLimitMiddleware(config));
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function withAuth(config) {
|
|
216
|
+
return (app) => {
|
|
217
|
+
app.use(createAuthMiddleware(config));
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Convenience functions for route-specific middleware
|
|
221
|
+
export function validateFields(rules) {
|
|
222
|
+
return createValidationMiddleware(rules);
|
|
223
|
+
}
|
|
224
|
+
export function rateLimit(config = {}) {
|
|
225
|
+
return createRateLimitMiddleware(config);
|
|
226
|
+
}
|
|
227
|
+
export function requireAuth(config) {
|
|
228
|
+
return createAuthMiddleware(config);
|
|
229
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PeriodicHealthCheckConfig } from './types';
|
|
2
|
+
export declare class PeriodicHealthMonitor {
|
|
3
|
+
private intervals;
|
|
4
|
+
private config;
|
|
5
|
+
private serviceName;
|
|
6
|
+
constructor(config: PeriodicHealthCheckConfig, serviceName: string);
|
|
7
|
+
start(): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
private checkServiceHealth;
|
|
10
|
+
getHealthStatus(): Promise<Record<string, boolean>>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class PeriodicHealthMonitor {
|
|
2
|
+
constructor(config, serviceName) {
|
|
3
|
+
this.intervals = [];
|
|
4
|
+
this.config = config;
|
|
5
|
+
this.serviceName = serviceName;
|
|
6
|
+
}
|
|
7
|
+
start() {
|
|
8
|
+
if (!this.config.enabled || !this.config.services?.length) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const interval = this.config.interval || 30000;
|
|
12
|
+
this.config.services.forEach(service => {
|
|
13
|
+
const intervalId = setInterval(async () => {
|
|
14
|
+
await this.checkServiceHealth(service);
|
|
15
|
+
}, interval);
|
|
16
|
+
this.intervals.push(intervalId);
|
|
17
|
+
});
|
|
18
|
+
console.log(`📊 ${this.serviceName}: Periodic health monitoring enabled (${interval}ms interval) for ${this.config.services.length} service(s)`);
|
|
19
|
+
}
|
|
20
|
+
stop() {
|
|
21
|
+
this.intervals.forEach(interval => clearInterval(interval));
|
|
22
|
+
this.intervals = [];
|
|
23
|
+
console.log(`🛑 ${this.serviceName}: Periodic health monitoring stopped`);
|
|
24
|
+
}
|
|
25
|
+
async checkServiceHealth(service) {
|
|
26
|
+
try {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timeout = service.timeout || 5000;
|
|
29
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
30
|
+
const response = await fetch(service.url, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
headers: {
|
|
34
|
+
'User-Agent': `${this.serviceName}-health-monitor`
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
console.log(`🟢 ${service.name} is healthy`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(`🔴 ${service.name} returned ${response.status}`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
49
|
+
console.log(`🔴 ${service.name} health check failed: ${errorMessage}`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Get current health status of all services
|
|
54
|
+
async getHealthStatus() {
|
|
55
|
+
if (!this.config.services?.length) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
const results = {};
|
|
59
|
+
await Promise.all(this.config.services.map(async (service) => {
|
|
60
|
+
results[service.name] = await this.checkServiceHealth(service);
|
|
61
|
+
}));
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { Server } from 'http';
|
|
3
|
+
import { ServerConfig, SocketIOConfig } from './types';
|
|
4
|
+
export interface GrpcService {
|
|
5
|
+
service: Record<string, unknown>;
|
|
6
|
+
implementation: Record<string, (...args: unknown[]) => unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface RpcMethod {
|
|
9
|
+
[key: string]: (params: unknown[], callback: (error: Error | null, result?: unknown) => void) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface WebhookConfig {
|
|
12
|
+
path: string;
|
|
13
|
+
secret?: string;
|
|
14
|
+
handler: (payload: Record<string, unknown>, headers: Record<string, string | string[]>) => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface GrpcServerInstance {
|
|
17
|
+
start(): void;
|
|
18
|
+
forceShutdown(): void;
|
|
19
|
+
addService(service: unknown, implementation: unknown): void;
|
|
20
|
+
bindAsync(address: string, credentials: unknown, callback: () => void): void;
|
|
21
|
+
}
|
|
22
|
+
export interface ServerInstanceConfig extends Required<Omit<ServerConfig, 'socketIO' | 'name' | 'version'>> {
|
|
23
|
+
name: string;
|
|
24
|
+
version: string;
|
|
25
|
+
startTime: Date;
|
|
26
|
+
socketIO?: SocketIOConfig;
|
|
27
|
+
}
|
|
28
|
+
export interface ServerInstance {
|
|
29
|
+
app: express.Application;
|
|
30
|
+
server?: Server;
|
|
31
|
+
config: ServerInstanceConfig;
|
|
32
|
+
start(): Promise<ServerInstance>;
|
|
33
|
+
stop(): Promise<void>;
|
|
34
|
+
getInfo(): ServerInfo;
|
|
35
|
+
addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port?: number): void;
|
|
36
|
+
addRpcMethods(methods: RpcMethod, path?: string): void;
|
|
37
|
+
addWebhook(config: WebhookConfig): void;
|
|
38
|
+
addSocketIO(config?: SocketIOConfig): unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface ServerInfo {
|
|
41
|
+
name: string;
|
|
42
|
+
version: string;
|
|
43
|
+
port: number;
|
|
44
|
+
uptime: number;
|
|
45
|
+
status: 'starting' | 'running' | 'stopping' | 'stopped';
|
|
46
|
+
startTime: Date;
|
|
47
|
+
}
|
|
48
|
+
export declare class ExpressServer implements ServerInstance {
|
|
49
|
+
app: express.Application;
|
|
50
|
+
server?: Server;
|
|
51
|
+
config: ServerInstanceConfig;
|
|
52
|
+
private status;
|
|
53
|
+
private grpcServices;
|
|
54
|
+
private grpcServer?;
|
|
55
|
+
private rpcMethods;
|
|
56
|
+
private socketIO?;
|
|
57
|
+
private healthMonitor?;
|
|
58
|
+
constructor(name?: string, version?: string, config?: ServerConfig);
|
|
59
|
+
private setupMiddleware;
|
|
60
|
+
private setupPeriodicHealthMonitoring;
|
|
61
|
+
start(): Promise<ServerInstance>;
|
|
62
|
+
stop(): Promise<void>;
|
|
63
|
+
getInfo(): ServerInfo;
|
|
64
|
+
addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port?: number): void;
|
|
65
|
+
addRpcMethods(methods: RpcMethod, path?: string): void;
|
|
66
|
+
addWebhook(config: WebhookConfig): void;
|
|
67
|
+
addSocketIO(config?: SocketIOConfig): unknown;
|
|
68
|
+
}
|
|
69
|
+
export declare function createServer(name?: string, version?: string, config?: ServerConfig): ServerInstance;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createGracefulShutdown } from './shutdown';
|
|
3
|
+
import { PeriodicHealthMonitor } from './periodic-health';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
export class ExpressServer {
|
|
6
|
+
constructor(name = 'Express Server', version = '1.0.0', config = {}) {
|
|
7
|
+
this.status = 'stopped';
|
|
8
|
+
this.grpcServices = [];
|
|
9
|
+
this.rpcMethods = {};
|
|
10
|
+
this.app = express();
|
|
11
|
+
this.config = {
|
|
12
|
+
name,
|
|
13
|
+
version,
|
|
14
|
+
startTime: new Date(),
|
|
15
|
+
port: config.port || 3000,
|
|
16
|
+
cors: config.cors ?? true,
|
|
17
|
+
helmet: config.helmet ?? true,
|
|
18
|
+
json: config.json ?? true,
|
|
19
|
+
cookieParser: config.cookieParser ?? false,
|
|
20
|
+
customMiddleware: config.customMiddleware || [],
|
|
21
|
+
healthCheck: config.healthCheck ?? true,
|
|
22
|
+
gracefulShutdown: config.gracefulShutdown ?? true,
|
|
23
|
+
socketIO: config.socketIO,
|
|
24
|
+
periodicHealthCheck: config.periodicHealthCheck || { enabled: false }
|
|
25
|
+
};
|
|
26
|
+
// Apply middleware based on configuration
|
|
27
|
+
this.setupMiddleware();
|
|
28
|
+
// Setup periodic health monitoring
|
|
29
|
+
this.setupPeriodicHealthMonitoring();
|
|
30
|
+
}
|
|
31
|
+
setupMiddleware() {
|
|
32
|
+
// Apply CORS if enabled
|
|
33
|
+
if (this.config.cors) {
|
|
34
|
+
try {
|
|
35
|
+
const cors = require('cors');
|
|
36
|
+
const corsOptions = typeof this.config.cors === 'object' ? this.config.cors : undefined;
|
|
37
|
+
this.app.use(cors(corsOptions));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn(`${this.config.name}: CORS middleware not available. Install cors package.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Apply Helmet if enabled
|
|
44
|
+
if (this.config.helmet) {
|
|
45
|
+
try {
|
|
46
|
+
const helmet = require('helmet');
|
|
47
|
+
this.app.use(helmet());
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn(`${this.config.name}: Helmet middleware not available. Install helmet package.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Apply JSON parser if enabled
|
|
54
|
+
if (this.config.json) {
|
|
55
|
+
this.app.use(express.json());
|
|
56
|
+
}
|
|
57
|
+
// Apply cookie parser if enabled
|
|
58
|
+
if (this.config.cookieParser) {
|
|
59
|
+
try {
|
|
60
|
+
const cookieParser = require('cookie-parser');
|
|
61
|
+
this.app.use(cookieParser());
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.warn(`${this.config.name}: Cookie parser middleware not available. Install cookie-parser package.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Apply custom middleware
|
|
68
|
+
if (this.config.customMiddleware && this.config.customMiddleware.length > 0) {
|
|
69
|
+
this.config.customMiddleware.forEach(middleware => {
|
|
70
|
+
this.app.use(middleware);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Add health check if enabled
|
|
74
|
+
if (this.config.healthCheck) {
|
|
75
|
+
const healthPath = typeof this.config.healthCheck === 'string' ? this.config.healthCheck : '/health';
|
|
76
|
+
this.app.get(healthPath, (req, res) => {
|
|
77
|
+
res.status(200).json({
|
|
78
|
+
status: 'healthy',
|
|
79
|
+
service: this.config.name,
|
|
80
|
+
version: this.config.version,
|
|
81
|
+
uptime: Date.now() - this.config.startTime.getTime(),
|
|
82
|
+
timestamp: new Date().toISOString()
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setupPeriodicHealthMonitoring() {
|
|
88
|
+
if (this.config.periodicHealthCheck?.enabled) {
|
|
89
|
+
this.healthMonitor = new PeriodicHealthMonitor(this.config.periodicHealthCheck, this.config.name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async start() {
|
|
93
|
+
this.status = 'starting';
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
try {
|
|
96
|
+
this.server = this.app.listen(this.config.port, () => {
|
|
97
|
+
this.status = 'running';
|
|
98
|
+
console.log(`🚀 ${this.config.name} v${this.config.version} running on http://localhost:${this.config.port}`);
|
|
99
|
+
if (this.config.gracefulShutdown) {
|
|
100
|
+
createGracefulShutdown(this.server, {
|
|
101
|
+
onShutdown: async () => {
|
|
102
|
+
this.status = 'stopping';
|
|
103
|
+
// Stop health monitoring during shutdown
|
|
104
|
+
if (this.healthMonitor) {
|
|
105
|
+
this.healthMonitor.stop();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Start periodic health monitoring after server is running
|
|
111
|
+
if (this.healthMonitor) {
|
|
112
|
+
this.healthMonitor.start();
|
|
113
|
+
}
|
|
114
|
+
resolve(this);
|
|
115
|
+
});
|
|
116
|
+
this.server.on('error', reject);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.status = 'stopped';
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async stop() {
|
|
125
|
+
this.status = 'stopping';
|
|
126
|
+
// Stop gRPC server if running
|
|
127
|
+
if (this.grpcServer) {
|
|
128
|
+
this.grpcServer.forceShutdown();
|
|
129
|
+
}
|
|
130
|
+
// Stop periodic health monitoring
|
|
131
|
+
if (this.healthMonitor) {
|
|
132
|
+
this.healthMonitor.stop();
|
|
133
|
+
}
|
|
134
|
+
// Stop Socket.IO server if running
|
|
135
|
+
if (this.socketIO) {
|
|
136
|
+
this.socketIO.close();
|
|
137
|
+
}
|
|
138
|
+
if (!this.server) {
|
|
139
|
+
this.status = 'stopped';
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
this.server.close(() => {
|
|
144
|
+
this.status = 'stopped';
|
|
145
|
+
console.log(`👋 ${this.config.name} stopped`);
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
getInfo() {
|
|
151
|
+
return {
|
|
152
|
+
name: this.config.name,
|
|
153
|
+
version: this.config.version,
|
|
154
|
+
port: this.config.port,
|
|
155
|
+
uptime: Date.now() - this.config.startTime.getTime(),
|
|
156
|
+
status: this.status,
|
|
157
|
+
startTime: this.config.startTime
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
addGrpcService(service, implementation, port = 50051) {
|
|
161
|
+
this.grpcServices.push({ service, implementation });
|
|
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');
|
|
167
|
+
this.grpcServer = new grpc.Server();
|
|
168
|
+
// Add all services
|
|
169
|
+
this.grpcServices.forEach(({ service, implementation }) => {
|
|
170
|
+
this.grpcServer.addService(service, implementation);
|
|
171
|
+
});
|
|
172
|
+
this.grpcServer.bindAsync(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure(), () => {
|
|
173
|
+
this.grpcServer.start();
|
|
174
|
+
console.log(`🔗 ${this.config.name} gRPC server running on port ${port}`);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
console.warn(`${this.config.name}: gRPC not available. Install @grpc/grpc-js to use gRPC features.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
addRpcMethods(methods, path = '/rpc') {
|
|
183
|
+
Object.assign(this.rpcMethods, methods);
|
|
184
|
+
try {
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
186
|
+
const jayson = require('jayson');
|
|
187
|
+
const rpcServer = jayson.server(this.rpcMethods);
|
|
188
|
+
this.app.use(path, rpcServer.middleware());
|
|
189
|
+
console.log(`📡 ${this.config.name} JSON-RPC server mounted on ${path}`);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.warn(`${this.config.name}: JSON-RPC not available. Install jayson to use RPC features.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
addWebhook(config) {
|
|
196
|
+
this.app.post(config.path, express.raw({ type: 'application/json' }), async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
// Verify signature if secret provided
|
|
199
|
+
if (config.secret) {
|
|
200
|
+
const signature = req.headers['x-hub-signature-256'] || req.headers['x-signature-256'];
|
|
201
|
+
if (signature) {
|
|
202
|
+
const expectedSignature = crypto
|
|
203
|
+
.createHmac('sha256', config.secret)
|
|
204
|
+
.update(req.body)
|
|
205
|
+
.digest('hex');
|
|
206
|
+
const providedSignature = Array.isArray(signature) ? signature[0] : signature;
|
|
207
|
+
if (!providedSignature.includes(expectedSignature)) {
|
|
208
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Parse JSON payload
|
|
213
|
+
const payload = JSON.parse(req.body.toString());
|
|
214
|
+
// Call handler
|
|
215
|
+
await config.handler(payload, req.headers);
|
|
216
|
+
res.status(200).json({ success: true });
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error('Webhook error:', error);
|
|
220
|
+
res.status(500).json({ error: 'Webhook processing failed' });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
console.log(`🪝 ${this.config.name} webhook registered at ${config.path}${config.secret ? ' (with signature verification)' : ''}`);
|
|
224
|
+
}
|
|
225
|
+
addSocketIO(config = {}) {
|
|
226
|
+
if (!this.server) {
|
|
227
|
+
throw new Error(`${this.config.name}: Server must be started before adding Socket.IO`);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
231
|
+
const { Server } = require('socket.io');
|
|
232
|
+
// Configure CORS
|
|
233
|
+
const corsConfig = config.cors === true
|
|
234
|
+
? { origin: '*', methods: ['GET', 'POST'] }
|
|
235
|
+
: config.cors || undefined;
|
|
236
|
+
// Create Socket.IO server
|
|
237
|
+
const io = new Server(this.server, {
|
|
238
|
+
cors: config.cors ? corsConfig : undefined,
|
|
239
|
+
path: config.path || '/socket.io'
|
|
240
|
+
});
|
|
241
|
+
// Store reference for cleanup
|
|
242
|
+
this.socketIO = io;
|
|
243
|
+
// Handle connections
|
|
244
|
+
io.on('connection', (socket) => {
|
|
245
|
+
const typedSocket = socket;
|
|
246
|
+
console.log(`🔌 ${this.config.name}: Socket connected [${typedSocket.id}]`);
|
|
247
|
+
// Call user-defined connection handler
|
|
248
|
+
if (config.onConnection) {
|
|
249
|
+
config.onConnection(socket);
|
|
250
|
+
}
|
|
251
|
+
// Handle disconnection
|
|
252
|
+
typedSocket.on('disconnect', (reason) => {
|
|
253
|
+
console.log(`🔌 ${this.config.name}: Socket disconnected [${typedSocket.id}] - ${reason}`);
|
|
254
|
+
// Call user-defined disconnection handler
|
|
255
|
+
if (config.onDisconnection) {
|
|
256
|
+
config.onDisconnection(socket, reason);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
console.log(`🔌 ${this.config.name} Socket.IO server attached${config.path ? ` at ${config.path}` : ''}${config.cors ? ' (CORS enabled)' : ''}`);
|
|
261
|
+
return io;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.warn(`${this.config.name}: Socket.IO not available. Install socket.io to use WebSocket features.`);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function createServer(name, version, config) {
|
|
270
|
+
return new ExpressServer(name, version, config);
|
|
271
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Server } from 'http';
|
|
2
|
+
import { GracefulShutdownConfig, ServerPlugin } from './types';
|
|
3
|
+
export declare function createGracefulShutdown(server: Server, config?: GracefulShutdownConfig): void;
|
|
4
|
+
export declare function withGracefulShutdown(config?: GracefulShutdownConfig): ServerPlugin;
|
|
5
|
+
export declare function startServerWithShutdown(app: any, port: number, shutdownConfig?: GracefulShutdownConfig, serverName?: string, serverVersion?: string): Server;
|