opticore-api-gateway 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.
package/dist/index.js ADDED
@@ -0,0 +1,1056 @@
1
+ // src/core/gateway.core.ts
2
+ import * as http2 from "http";
3
+ import { OpticoreRegisterRouter, OpticoreStandaloneRouterFactory as OpticoreStandaloneRouterFactory2 } from "opticore-router";
4
+
5
+ // src/infrastructure/services/strategies/serviceRegistry.strategy.ts
6
+ var ServiceRegistry = class {
7
+ services = /* @__PURE__ */ new Map();
8
+ serviceHealthChecks = /* @__PURE__ */ new Map();
9
+ circuitBreakers = /* @__PURE__ */ new Map();
10
+ registerService(service) {
11
+ if (!this.services.has(service.name)) {
12
+ this.services.set(service.name, []);
13
+ }
14
+ const instance = {
15
+ ...service,
16
+ healthy: true,
17
+ currentConnections: 0,
18
+ lastHealthCheck: /* @__PURE__ */ new Date(),
19
+ failureCount: 0,
20
+ circuitState: "closed"
21
+ };
22
+ this.services.get(service.name).push(instance);
23
+ this.circuitBreakers.set(`${service.name}-${service.url}`, new CircuitBreaker());
24
+ if (service.healthCheck) {
25
+ this.startHealthCheck(service.name, instance);
26
+ }
27
+ }
28
+ startHealthCheck(serviceName, instance) {
29
+ const healthCheck = setInterval(async () => {
30
+ try {
31
+ instance.healthy = true;
32
+ instance.lastHealthCheck = /* @__PURE__ */ new Date();
33
+ } catch (error) {
34
+ instance.healthy = false;
35
+ console.error(`Health check failed for ${serviceName}: ${error}`);
36
+ }
37
+ }, 3e4);
38
+ this.serviceHealthChecks.set(`${serviceName}-${instance.url}`, healthCheck);
39
+ }
40
+ getServiceInstances(serviceName) {
41
+ return this.services.get(serviceName) || [];
42
+ }
43
+ getHealthyInstances(serviceName) {
44
+ const instances = this.services.get(serviceName) || [];
45
+ const now = /* @__PURE__ */ new Date();
46
+ return instances.filter((instance) => {
47
+ const circuitBreaker = this.circuitBreakers.get(`${serviceName}-${instance.url}`);
48
+ if (circuitBreaker && !circuitBreaker.allowRequest()) {
49
+ return false;
50
+ }
51
+ return instance.healthy;
52
+ });
53
+ }
54
+ recordSuccess(serviceName, url) {
55
+ const circuitBreaker = this.circuitBreakers.get(`${serviceName}-${url}`);
56
+ if (circuitBreaker) {
57
+ circuitBreaker.recordSuccess();
58
+ }
59
+ }
60
+ recordFailure(serviceName, url) {
61
+ const circuitBreaker = this.circuitBreakers.get(`${serviceName}-${url}`);
62
+ if (circuitBreaker) {
63
+ circuitBreaker.recordFailure();
64
+ }
65
+ const instances = this.services.get(serviceName);
66
+ if (instances) {
67
+ const instance = instances.find((inst) => inst.url === url);
68
+ if (instance) {
69
+ instance.failureCount++;
70
+ }
71
+ }
72
+ }
73
+ deregisterService(serviceName, url) {
74
+ const instances = this.services.get(serviceName);
75
+ if (instances) {
76
+ const filtered = instances.filter((instance) => instance.url !== url);
77
+ this.services.set(serviceName, filtered);
78
+ const healthCheckKey = `${serviceName}-${url}`;
79
+ const interval = this.serviceHealthChecks.get(healthCheckKey);
80
+ if (interval) {
81
+ clearInterval(interval);
82
+ this.serviceHealthChecks.delete(healthCheckKey);
83
+ }
84
+ this.circuitBreakers.delete(healthCheckKey);
85
+ }
86
+ }
87
+ getAllServices() {
88
+ return new Map(this.services);
89
+ }
90
+ };
91
+ var CircuitBreaker = class {
92
+ state = "closed";
93
+ failureCount = 0;
94
+ successCount = 0;
95
+ lastFailureTime = null;
96
+ failureThreshold = 5;
97
+ resetTimeout = 3e4;
98
+ halfOpenMaxAttempts = 3;
99
+ allowRequest() {
100
+ if (this.state === "closed") {
101
+ return true;
102
+ }
103
+ if (this.state === "open") {
104
+ if (this.lastFailureTime) {
105
+ const now = /* @__PURE__ */ new Date();
106
+ const timeSinceFailure = now.getTime() - this.lastFailureTime.getTime();
107
+ if (timeSinceFailure > this.resetTimeout) {
108
+ this.state = "half-open";
109
+ this.successCount = 0;
110
+ return true;
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+ return this.successCount < this.halfOpenMaxAttempts;
116
+ }
117
+ recordSuccess() {
118
+ if (this.state === "half-open") {
119
+ this.successCount++;
120
+ if (this.successCount >= this.halfOpenMaxAttempts) {
121
+ this.reset();
122
+ }
123
+ } else {
124
+ this.failureCount = Math.max(0, this.failureCount - 1);
125
+ }
126
+ }
127
+ recordFailure() {
128
+ this.failureCount++;
129
+ this.lastFailureTime = /* @__PURE__ */ new Date();
130
+ if (this.failureCount >= this.failureThreshold) {
131
+ this.state = "open";
132
+ } else if (this.state === "half-open") {
133
+ this.state = "open";
134
+ this.successCount = 0;
135
+ }
136
+ }
137
+ reset() {
138
+ this.state = "closed";
139
+ this.failureCount = 0;
140
+ this.successCount = 0;
141
+ this.lastFailureTime = null;
142
+ }
143
+ };
144
+
145
+ // src/infrastructure/services/strategies/loadBalancer.strategy.ts
146
+ var LoadBalancer = class {
147
+ strategy;
148
+ counters = /* @__PURE__ */ new Map();
149
+ constructor(strategy = "round-robin") {
150
+ this.strategy = strategy;
151
+ }
152
+ /**
153
+ *
154
+ * @param serviceName
155
+ * @param instances
156
+ */
157
+ selectInstance(serviceName, instances) {
158
+ if (instances.length === 0) return null;
159
+ switch (this.strategy) {
160
+ case "round-robin":
161
+ return this.roundRobin(serviceName, instances);
162
+ case "random":
163
+ return this.random(instances);
164
+ case "weighted":
165
+ return this.weighted(instances);
166
+ case "least-connections":
167
+ return this.leastConnections(instances);
168
+ default:
169
+ return this.roundRobin(serviceName, instances);
170
+ }
171
+ }
172
+ /**
173
+ *
174
+ * @param serviceName
175
+ * @param instances
176
+ * @private
177
+ */
178
+ roundRobin(serviceName, instances) {
179
+ const counter = this.counters.get(serviceName) || 0;
180
+ const index = counter % instances.length;
181
+ this.counters.set(serviceName, counter + 1);
182
+ return instances[index];
183
+ }
184
+ /**
185
+ *
186
+ * @param instances
187
+ * @private
188
+ */
189
+ random(instances) {
190
+ const index = Math.floor(Math.random() * instances.length);
191
+ return instances[index];
192
+ }
193
+ /**
194
+ *
195
+ * @param instances
196
+ * @private
197
+ */
198
+ weighted(instances) {
199
+ const totalWeight = instances.reduce((sum, instance) => sum + (instance.weight || 1), 0);
200
+ let random = Math.random() * totalWeight;
201
+ for (const instance of instances) {
202
+ random -= instance.weight || 1;
203
+ if (random <= 0) {
204
+ return instance;
205
+ }
206
+ }
207
+ return instances[0];
208
+ }
209
+ /**
210
+ *
211
+ * @param instances
212
+ * @private
213
+ */
214
+ leastConnections(instances) {
215
+ return instances.reduce(
216
+ (prev, current) => prev.currentConnections < current.currentConnections ? prev : current
217
+ );
218
+ }
219
+ /**
220
+ *
221
+ * @param strategy
222
+ */
223
+ setStrategy(strategy) {
224
+ this.strategy = strategy;
225
+ }
226
+ };
227
+
228
+ // src/utils/httpClient.utils.ts
229
+ import * as http from "http";
230
+ import * as https from "https";
231
+ import { Buffer as Buffer2 } from "buffer";
232
+ var HttpClient = class {
233
+ /**
234
+ *
235
+ * @param url
236
+ * @param options
237
+ */
238
+ async request(url, options) {
239
+ return new Promise((resolve, reject) => {
240
+ try {
241
+ const urlObj = new URL(url);
242
+ const isHttps = urlObj.protocol === "https:";
243
+ const headers = {
244
+ "User-Agent": "API-Gateway/1.0",
245
+ "Accept": "application/json",
246
+ ...options.headers
247
+ };
248
+ let requestBody;
249
+ if (options.body !== void 0 && options.body !== null) {
250
+ if (typeof options.body === "string") {
251
+ requestBody = options.body;
252
+ headers["Content-Type"] = headers["Content-Type"] || "text/plain";
253
+ } else if (Buffer2.isBuffer(options.body)) {
254
+ requestBody = options.body;
255
+ } else if (typeof options.body === "object") {
256
+ requestBody = JSON.stringify(options.body);
257
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
258
+ }
259
+ if (requestBody) {
260
+ headers["Content-Length"] = Buffer2.byteLength(requestBody).toString();
261
+ }
262
+ }
263
+ const requestOptions = {
264
+ method: options.method.toUpperCase(),
265
+ headers,
266
+ timeout: options.timeout || 1e4
267
+ };
268
+ const client = isHttps ? https : http;
269
+ const req = client.request(
270
+ url,
271
+ requestOptions,
272
+ (res) => {
273
+ const responseHeaders = {};
274
+ Object.keys(res.headers).forEach((key) => {
275
+ const value = res.headers[key];
276
+ if (value !== void 0) {
277
+ responseHeaders[key] = value;
278
+ }
279
+ });
280
+ const chunks = [];
281
+ res.on("data", (chunk) => {
282
+ chunks.push(chunk);
283
+ });
284
+ res.on("end", () => {
285
+ const body = Buffer2.concat(chunks).toString("utf8");
286
+ resolve({
287
+ status: res.statusCode || 500,
288
+ headers: responseHeaders,
289
+ body
290
+ });
291
+ });
292
+ res.on("error", (error) => {
293
+ reject(error);
294
+ });
295
+ }
296
+ );
297
+ req.on("error", (error) => {
298
+ reject(error);
299
+ });
300
+ req.on("timeout", () => {
301
+ req.destroy();
302
+ reject(new Error(`Request timeout after ${requestOptions.timeout}ms`));
303
+ });
304
+ if (requestBody) {
305
+ req.write(requestBody);
306
+ }
307
+ req.end();
308
+ } catch (error) {
309
+ reject(error instanceof Error ? error : new Error(String(error)));
310
+ }
311
+ });
312
+ }
313
+ /**
314
+ *
315
+ * @param method
316
+ * @private
317
+ */
318
+ getDefaultHeaders(method) {
319
+ const headers = {};
320
+ if (["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
321
+ headers["Content-Type"] = "application/json";
322
+ }
323
+ return headers;
324
+ }
325
+ /**
326
+ *
327
+ * @param req
328
+ * @param body
329
+ * @param headers
330
+ * @private
331
+ */
332
+ writeRequestBody(req, body, headers) {
333
+ const contentType = this.getContentType(headers);
334
+ try {
335
+ if (Buffer2.isBuffer(body)) {
336
+ req.write(body);
337
+ return;
338
+ }
339
+ if (typeof body === "string") {
340
+ req.write(body);
341
+ return;
342
+ }
343
+ if (contentType?.includes("application/json")) {
344
+ if (typeof body === "object" && body !== null) {
345
+ req.write(JSON.stringify(body));
346
+ } else {
347
+ req.write(String(body));
348
+ }
349
+ } else if (contentType?.includes("application/x-www-form-urlencoded")) {
350
+ if (typeof body === "object" && body !== null) {
351
+ const params = new URLSearchParams();
352
+ for (const [key, value] of Object.entries(body)) {
353
+ if (value !== void 0 && value !== null) {
354
+ params.append(key, String(value));
355
+ }
356
+ }
357
+ req.write(params.toString());
358
+ } else {
359
+ req.write(String(body));
360
+ }
361
+ } else if (contentType?.includes("text/")) {
362
+ req.write(String(body));
363
+ } else {
364
+ if (typeof body === "object" && body !== null) {
365
+ req.write(JSON.stringify(body));
366
+ } else {
367
+ req.write(String(body));
368
+ }
369
+ }
370
+ } catch (error) {
371
+ console.error("Error writing request body:", error);
372
+ throw new Error(`Failed to write request body: ${error.message}`);
373
+ }
374
+ }
375
+ /**
376
+ *
377
+ * @param headers
378
+ * @private
379
+ */
380
+ getContentType(headers) {
381
+ const contentType = headers["Content-Type"] || headers["content-type"];
382
+ if (!contentType) return void 0;
383
+ if (Array.isArray(contentType)) {
384
+ return contentType[0];
385
+ }
386
+ return contentType;
387
+ }
388
+ /**
389
+ *
390
+ * @param url
391
+ * @param headers
392
+ */
393
+ async get(url, headers) {
394
+ return this.request(url, {
395
+ method: "GET",
396
+ headers: headers || {}
397
+ });
398
+ }
399
+ /**
400
+ *
401
+ * @param url
402
+ * @param body
403
+ * @param headers
404
+ */
405
+ async post(url, body, headers) {
406
+ return this.request(url, {
407
+ method: "POST",
408
+ headers: headers || {},
409
+ body
410
+ });
411
+ }
412
+ /**
413
+ *
414
+ * @param url
415
+ * @param body
416
+ * @param headers
417
+ */
418
+ async put(url, body, headers) {
419
+ return this.request(url, {
420
+ method: "PUT",
421
+ headers: headers || {},
422
+ body
423
+ });
424
+ }
425
+ /**
426
+ *
427
+ * @param url
428
+ * @param headers
429
+ */
430
+ async delete(url, headers) {
431
+ return this.request(url, {
432
+ method: "DELETE",
433
+ headers: headers || {}
434
+ });
435
+ }
436
+ /**
437
+ *
438
+ * @param url
439
+ * @param body
440
+ * @param headers
441
+ */
442
+ async patch(url, body, headers) {
443
+ return this.request(url, {
444
+ method: "PATCH",
445
+ headers: headers || {},
446
+ body
447
+ });
448
+ }
449
+ };
450
+
451
+ // src/core/middleware.core.ts
452
+ var BaseMiddleware = class {
453
+ };
454
+ var MiddlewareChain = class {
455
+ middlewares = [];
456
+ add(middleware) {
457
+ this.middlewares.push(middleware);
458
+ }
459
+ async execute(req, res, finalHandler) {
460
+ let index = 0;
461
+ const next = () => {
462
+ if (index < this.middlewares.length) {
463
+ const middleware = this.middlewares[index++];
464
+ try {
465
+ const result = middleware(req, res, next);
466
+ if (result instanceof Promise) {
467
+ result.catch((error) => {
468
+ console.error("Middleware error:", error);
469
+ res.statusCode = 500;
470
+ res.end(JSON.stringify({ error: "Internal Server Error" }));
471
+ });
472
+ }
473
+ } catch (error) {
474
+ console.error("Middleware error:", error);
475
+ res.statusCode = 500;
476
+ res.end(JSON.stringify({ error: "Internal Server Error" }));
477
+ }
478
+ } else {
479
+ finalHandler(req, res, () => {
480
+ });
481
+ }
482
+ };
483
+ next();
484
+ }
485
+ getAll() {
486
+ return [...this.middlewares];
487
+ }
488
+ };
489
+
490
+ // src/core/gatewayRoute.core.ts
491
+ import {
492
+ OpticoreRouterCollectionRouterFactory,
493
+ OpticoreStandaloneRouterFactory
494
+ } from "opticore-router";
495
+ import { LoggerCore } from "opticore-logger";
496
+ import { HttpStatusCode } from "opticore-http-response";
497
+ import { TranslationLoader } from "opticore-translator";
498
+ var GatewayRoute = class {
499
+ localLanguage;
500
+ config;
501
+ standaloneRouter;
502
+ collectionRouter = null;
503
+ routeHandler;
504
+ logger = new LoggerCore();
505
+ /**
506
+ *
507
+ * @param config
508
+ * @param routeHandler
509
+ * @param localLanguage
510
+ */
511
+ constructor(config, routeHandler, localLanguage) {
512
+ this.localLanguage = localLanguage;
513
+ this.config = config;
514
+ this.routeHandler = routeHandler;
515
+ this.standaloneRouter = new OpticoreStandaloneRouterFactory();
516
+ this.logger = new LoggerCore();
517
+ this.standaloneRouter.storeRoute(
518
+ config.method,
519
+ config.path,
520
+ async (context) => routeHandler(context.req, context.res, context.next),
521
+ false
522
+ );
523
+ }
524
+ /**
525
+ * Method for creating a multi-router configuration
526
+ *
527
+ * @param controller
528
+ */
529
+ createMultipleRouterConfig(controller) {
530
+ return {
531
+ path: this.config.path,
532
+ method: this.config.method,
533
+ middlewares: this.config.middlewares || [],
534
+ handler: async (context) => {
535
+ return await this.routeHandler(context.req, context.res, context.next);
536
+ }
537
+ };
538
+ }
539
+ /**
540
+ * Method for creating a router collection
541
+ *
542
+ * @param controller
543
+ * @param routes
544
+ */
545
+ createCollectionRouter(controller, routes) {
546
+ this.collectionRouter = new OpticoreRouterCollectionRouterFactory(
547
+ controller,
548
+ routes
549
+ );
550
+ }
551
+ /**
552
+ * Get the standalone router
553
+ *
554
+ * @param strategy
555
+ * @param options
556
+ */
557
+ getStandaloneRoute(strategy, options) {
558
+ return this.standaloneRouter.getRoute(strategy, options);
559
+ }
560
+ /**
561
+ * Get the definition of multiple routes
562
+ */
563
+ getMultipleRouteDefinition() {
564
+ if (!this.collectionRouter) {
565
+ this.logger.error({
566
+ errorType: TranslationLoader.t("MISSING_ROUTES", this.localLanguage),
567
+ httpCodeValue: HttpStatusCode.NOT_FOUND,
568
+ message: TranslationLoader.t("ROUTER_COLLECTION_NOT_FOUND", this.localLanguage),
569
+ stackTrace: void 0,
570
+ title: TranslationLoader.t("ROUTER_COLLECTION", this.localLanguage)
571
+ });
572
+ throw new Error(TranslationLoader.t("ROUTER_COLLECTION_NOT_FOUND", this.localLanguage));
573
+ }
574
+ return this.collectionRouter.getRoute();
575
+ }
576
+ /**
577
+ *
578
+ */
579
+ getConfig() {
580
+ return this.config;
581
+ }
582
+ /**
583
+ * Utility method for creating an Opticore-compatible route
584
+ *
585
+ * @param path
586
+ * @param handler
587
+ */
588
+ static createOpticoreRouteDefinition(path, handler) {
589
+ return {
590
+ path,
591
+ handler: async (context) => {
592
+ return await handler(context.req, context.res, context.next);
593
+ }
594
+ };
595
+ }
596
+ };
597
+
598
+ // src/core/gateway.core.ts
599
+ var APIGateway = class {
600
+ config;
601
+ server = null;
602
+ serviceRegistry;
603
+ loadBalancer;
604
+ httpClient;
605
+ middlewareChain;
606
+ routes = /* @__PURE__ */ new Map();
607
+ opticoreRoutes = [];
608
+ registerRouter;
609
+ constructor(config) {
610
+ this.config = config;
611
+ this.serviceRegistry = new ServiceRegistry();
612
+ this.loadBalancer = new LoadBalancer(config.loadBalancer || "round-robin");
613
+ this.httpClient = new HttpClient();
614
+ this.middlewareChain = new MiddlewareChain();
615
+ this.registerRouter = new OpticoreRegisterRouter();
616
+ this.initializeServices();
617
+ this.initializeGlobalMiddlewares();
618
+ this.initializeRoutes();
619
+ this.buildOpticoreRoutes();
620
+ }
621
+ initializeServices() {
622
+ this.config.services.forEach((service) => {
623
+ this.serviceRegistry.registerService(service);
624
+ });
625
+ }
626
+ initializeGlobalMiddlewares() {
627
+ if (this.config.globalMiddlewares) {
628
+ this.config.globalMiddlewares.forEach((middleware) => {
629
+ if (typeof middleware === "function") {
630
+ this.middlewareChain.add(middleware);
631
+ } else if (middleware.handle && typeof middleware.handle === "function") {
632
+ this.middlewareChain.add(middleware.handle());
633
+ }
634
+ });
635
+ }
636
+ }
637
+ initializeRoutes() {
638
+ this.config.routes.forEach((routeConfig) => {
639
+ const handler = async (req, res, next) => {
640
+ await this.handleGatewayRequest(req, res, routeConfig);
641
+ };
642
+ const route = new GatewayRoute(routeConfig, handler, "");
643
+ this.routes.set(`${routeConfig.method}:${routeConfig.path}`, route);
644
+ });
645
+ }
646
+ buildOpticoreRoutes() {
647
+ const featureRoutes = [];
648
+ const gatewayFeature = { routes: [] };
649
+ this.routes.forEach((gatewayRoute) => {
650
+ const config = gatewayRoute.getConfig();
651
+ const standaloneRouter = new OpticoreStandaloneRouterFactory2();
652
+ const contextHandler = async (context) => {
653
+ const { req, res } = context;
654
+ req.url = req.originalUrl || req.url;
655
+ return new Promise((resolve) => {
656
+ this.middlewareChain.execute(req, res, (_r, _s, _n) => {
657
+ this.handleGatewayRequest(req, res, config).then(resolve).catch(() => {
658
+ if (!res.headersSent) {
659
+ res.statusCode = 502;
660
+ res.end(JSON.stringify({ error: "Bad Gateway" }));
661
+ }
662
+ resolve();
663
+ });
664
+ });
665
+ });
666
+ };
667
+ standaloneRouter.storeRoute(
668
+ config.method,
669
+ "*",
670
+ contextHandler
671
+ );
672
+ gatewayFeature.routes.push({
673
+ path: config.path,
674
+ handler: standaloneRouter.getRoute()
675
+ });
676
+ });
677
+ featureRoutes.push(gatewayFeature);
678
+ this.opticoreRoutes = this.registerRouter.registered(featureRoutes);
679
+ }
680
+ async handleGatewayRequest(req, res, routeConfig) {
681
+ const startTime = Date.now();
682
+ try {
683
+ if (routeConfig.middlewares && routeConfig.middlewares.length > 0) {
684
+ const routeMiddlewareChain = new MiddlewareChain();
685
+ routeConfig.middlewares.forEach((middleware) => {
686
+ if (typeof middleware === "function") {
687
+ routeMiddlewareChain.add(middleware);
688
+ } else if (middleware.handle && typeof middleware.handle === "function") {
689
+ routeMiddlewareChain.add(middleware.handle());
690
+ }
691
+ });
692
+ await new Promise((resolve, reject) => {
693
+ routeMiddlewareChain.execute(req, res, (req2, res2, next) => {
694
+ resolve();
695
+ });
696
+ });
697
+ }
698
+ let serviceUrl;
699
+ let serviceName;
700
+ if (Array.isArray(routeConfig.target)) {
701
+ serviceName = routeConfig.serviceName || this.extractServiceName(routeConfig.target[0]);
702
+ const instances = this.serviceRegistry.getHealthyInstances(serviceName);
703
+ if (instances.length === 0) {
704
+ res.statusCode = 503;
705
+ res.end(JSON.stringify({ error: "Service unavailable" }));
706
+ return;
707
+ }
708
+ const instance = this.loadBalancer.selectInstance(serviceName, instances);
709
+ if (!instance) {
710
+ res.statusCode = 503;
711
+ res.end(JSON.stringify({ error: "No healthy instances available" }));
712
+ return;
713
+ }
714
+ serviceUrl = this.rewriteUrl(req.url, instance.url, routeConfig.target[0]);
715
+ instance.currentConnections++;
716
+ const response = await this.forwardRequest(req, res, serviceUrl, instance);
717
+ this.serviceRegistry.recordSuccess(serviceName, instance.url);
718
+ instance.currentConnections--;
719
+ this.sendResponse(res, response);
720
+ } else {
721
+ serviceName = routeConfig.serviceName || this.extractServiceName(routeConfig.target);
722
+ const instances = this.serviceRegistry.getHealthyInstances(serviceName);
723
+ if (instances.length === 0) {
724
+ res.statusCode = 503;
725
+ res.end(JSON.stringify({ error: "Service unavailable" }));
726
+ return;
727
+ }
728
+ const instance = instances[0];
729
+ serviceUrl = this.rewriteUrl(req.url, instance.url, routeConfig.target);
730
+ instance.currentConnections++;
731
+ const response = await this.forwardRequest(req, res, serviceUrl, instance);
732
+ this.serviceRegistry.recordSuccess(serviceName, instance.url);
733
+ instance.currentConnections--;
734
+ this.sendResponse(res, response);
735
+ }
736
+ } catch (error) {
737
+ console.error("Gateway error:", error);
738
+ res.statusCode = 502;
739
+ res.end(JSON.stringify({
740
+ error: "Bad Gateway",
741
+ message: error instanceof Error ? error.message : "Unknown error"
742
+ }));
743
+ } finally {
744
+ const duration = Date.now() - startTime;
745
+ if (this.config.enableLogging) {
746
+ console.log(`Gateway request completed in ${duration}ms`);
747
+ }
748
+ }
749
+ }
750
+ extractServiceName(url) {
751
+ try {
752
+ const urlObj = new URL(url);
753
+ return urlObj.hostname;
754
+ } catch {
755
+ return "unknown-service";
756
+ }
757
+ }
758
+ rewriteUrl(originalUrl, baseUrl, pattern) {
759
+ if (pattern && originalUrl.startsWith(pattern)) {
760
+ const url = new URL(baseUrl);
761
+ const path = originalUrl.replace(new RegExp(`^${pattern}`), "");
762
+ return `${url.protocol}//${url.host}${path}`;
763
+ }
764
+ try {
765
+ const base = new URL(baseUrl);
766
+ return `${base.protocol}//${base.host}${originalUrl}`;
767
+ } catch {
768
+ return `${baseUrl}${originalUrl}`;
769
+ }
770
+ }
771
+ async forwardRequest(req, res, targetUrl, instance) {
772
+ const headers = { ...req.headers };
773
+ delete headers["host"];
774
+ delete headers["connection"];
775
+ headers["x-forwarded-for"] = req.socket?.remoteAddress || "";
776
+ headers["x-forwarded-proto"] = req.protocol || "http";
777
+ headers["x-gateway-service"] = instance.name || "unknown";
778
+ const timeout = instance.timeout || this.config.routes.find(
779
+ (r) => r.target === instance.url || Array.isArray(r.target) && r.target.includes(instance.url)
780
+ )?.timeout || 3e4;
781
+ try {
782
+ return await this.httpClient.request(targetUrl, {
783
+ method: req.method,
784
+ headers,
785
+ body: req.body,
786
+ timeout
787
+ });
788
+ } catch (error) {
789
+ this.serviceRegistry.recordFailure(instance.name, instance.url);
790
+ throw error;
791
+ }
792
+ }
793
+ sendResponse(res, httpResponse) {
794
+ if (httpResponse.headers) {
795
+ Object.keys(httpResponse.headers).forEach((key) => {
796
+ const value = httpResponse.headers[key];
797
+ if (value !== void 0) {
798
+ res.setHeader(key, value);
799
+ }
800
+ });
801
+ }
802
+ res.statusCode = httpResponse.status;
803
+ res.end(httpResponse.body);
804
+ }
805
+ addRoute(routeConfig) {
806
+ const handler = async (req, res, next) => {
807
+ await this.handleGatewayRequest(req, res, routeConfig);
808
+ };
809
+ const route = new GatewayRoute(routeConfig, handler, "");
810
+ this.routes.set(`${routeConfig.method}:${routeConfig.path}`, route);
811
+ this.buildOpticoreRoutes();
812
+ }
813
+ addMiddleware(middleware) {
814
+ this.middlewareChain.add(middleware);
815
+ }
816
+ registerService(service) {
817
+ this.serviceRegistry.registerService(service);
818
+ }
819
+ getOpticoreRoutes() {
820
+ return this.opticoreRoutes;
821
+ }
822
+ async parseRequestBody(req) {
823
+ if (req.body !== void 0) return;
824
+ return new Promise((resolve, reject) => {
825
+ const chunks = [];
826
+ req.on("data", (chunk) => chunks.push(chunk));
827
+ req.on("end", () => {
828
+ if (chunks.length === 0) {
829
+ resolve();
830
+ return;
831
+ }
832
+ const raw = Buffer.concat(chunks).toString("utf8");
833
+ const contentType = req.headers["content-type"] || "";
834
+ if (contentType.includes("application/json")) {
835
+ try {
836
+ req.body = JSON.parse(raw);
837
+ } catch {
838
+ req.body = raw;
839
+ }
840
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
841
+ req.body = Object.fromEntries(new URLSearchParams(raw));
842
+ } else {
843
+ req.body = raw || void 0;
844
+ }
845
+ resolve();
846
+ });
847
+ req.on("error", reject);
848
+ });
849
+ }
850
+ getExpressMiddleware() {
851
+ return (req, res, next) => {
852
+ this.middlewareChain.execute(req, res, (_r, _s, _n) => next());
853
+ };
854
+ }
855
+ start() {
856
+ return new Promise((resolve, reject) => {
857
+ this.server = http2.createServer(async (req, res) => {
858
+ await this.parseRequestBody(req);
859
+ this.middlewareChain.execute(req, res, () => {
860
+ let handled = false;
861
+ this.routes.forEach((gatewayRoute) => {
862
+ if (handled) return;
863
+ const config = gatewayRoute.getConfig();
864
+ if (req.method?.toLowerCase() === config.method && req.url?.startsWith(config.path)) {
865
+ this.handleGatewayRequest(req, res, config).catch(() => {
866
+ if (!res.headersSent) {
867
+ res.statusCode = 502;
868
+ res.end(JSON.stringify({ error: "Bad Gateway" }));
869
+ }
870
+ });
871
+ handled = true;
872
+ }
873
+ });
874
+ if (!handled) {
875
+ res.statusCode = 404;
876
+ res.end(JSON.stringify({ error: "Route not found" }));
877
+ }
878
+ });
879
+ });
880
+ this.server.listen(this.config.port, () => {
881
+ console.log(`API Gateway running on port ${this.config.port}`);
882
+ console.log(`Registered ${this.routes.size} routes`);
883
+ resolve();
884
+ });
885
+ this.server.on("error", (error) => {
886
+ reject(error);
887
+ });
888
+ });
889
+ }
890
+ stop() {
891
+ return new Promise((resolve, reject) => {
892
+ if (this.server) {
893
+ this.server.close((error) => {
894
+ if (error) {
895
+ reject(error);
896
+ } else {
897
+ console.log("API Gateway stopped");
898
+ this.server = null;
899
+ resolve();
900
+ }
901
+ });
902
+ } else {
903
+ resolve();
904
+ }
905
+ });
906
+ }
907
+ };
908
+
909
+ // src/presentation/middleware/auth.middleware.ts
910
+ var AuthMiddleware = class extends BaseMiddleware {
911
+ apiKeys;
912
+ constructor(apiKeys = []) {
913
+ super();
914
+ this.apiKeys = new Set(apiKeys);
915
+ }
916
+ handle() {
917
+ return (req, res, next) => {
918
+ const apiKey = req.headers["x-api-key"] || req.query && req.query.apiKey;
919
+ if (!apiKey) {
920
+ res.statusCode = 401;
921
+ res.end(JSON.stringify({ error: "API key required" }));
922
+ return;
923
+ }
924
+ if (!this.apiKeys.has(apiKey)) {
925
+ res.statusCode = 403;
926
+ res.end(JSON.stringify({ error: "Invalid API key" }));
927
+ return;
928
+ }
929
+ next();
930
+ };
931
+ }
932
+ };
933
+
934
+ // src/presentation/middleware/rateLimit.middleware.ts
935
+ var RateLimitMiddleware = class extends BaseMiddleware {
936
+ requestsPerWindow;
937
+ windowMs;
938
+ store;
939
+ constructor(requestsPerWindow = 100, windowMs = 6e4) {
940
+ super();
941
+ this.requestsPerWindow = requestsPerWindow;
942
+ this.windowMs = windowMs;
943
+ this.store = /* @__PURE__ */ new Map();
944
+ }
945
+ handle() {
946
+ return (req, res, next) => {
947
+ const clientId = req.headers["x-forwarded-for"] || req.socket?.remoteAddress || "unknown";
948
+ const key = `rate-limit:${clientId}`;
949
+ const now = Date.now();
950
+ let window = this.store.get(key);
951
+ if (!window || now > window.resetTime) {
952
+ window = {
953
+ count: 0,
954
+ resetTime: now + this.windowMs
955
+ };
956
+ this.store.set(key, window);
957
+ }
958
+ window.count++;
959
+ res.setHeader("X-RateLimit-Limit", this.requestsPerWindow.toString());
960
+ res.setHeader("X-RateLimit-Remaining", Math.max(0, this.requestsPerWindow - window.count).toString());
961
+ res.setHeader("X-RateLimit-Reset", Math.ceil(window.resetTime / 1e3).toString());
962
+ if (window.count > this.requestsPerWindow) {
963
+ res.statusCode = 429;
964
+ res.end(JSON.stringify({
965
+ error: "Too many requests",
966
+ retryAfter: Math.ceil((window.resetTime - now) / 1e3)
967
+ }));
968
+ return;
969
+ }
970
+ next();
971
+ };
972
+ }
973
+ };
974
+
975
+ // src/presentation/middleware/loggin.middleware.ts
976
+ var LoggingMiddleware = class extends BaseMiddleware {
977
+ constructor(logLevel = "info") {
978
+ super();
979
+ this.logLevel = logLevel;
980
+ }
981
+ handle() {
982
+ return (req, res, next) => {
983
+ const start = Date.now();
984
+ res.on("finish", () => {
985
+ const duration = Date.now() - start;
986
+ const message = `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`;
987
+ if (res.statusCode >= 500) console.error(message);
988
+ else console.log(message);
989
+ });
990
+ next();
991
+ };
992
+ }
993
+ };
994
+
995
+ // src/presentation/middleware/validation.middleware.ts
996
+ import { Validator } from "opticore-validator";
997
+ var ValidationMiddleware = class extends BaseMiddleware {
998
+ validator;
999
+ target;
1000
+ /**
1001
+ * Crée un middleware de validation.
1002
+ * @param schema - Le schéma de validation au format opticore-validator.
1003
+ * @param target - La partie de la requête à valider (par défaut 'body').
1004
+ */
1005
+ constructor(schema, target = "body") {
1006
+ super();
1007
+ this.validator = new Validator(schema);
1008
+ this.target = target;
1009
+ }
1010
+ handle() {
1011
+ return (req, res, next) => {
1012
+ let dataToValidate;
1013
+ switch (this.target) {
1014
+ case "body":
1015
+ dataToValidate = req.body;
1016
+ break;
1017
+ case "query":
1018
+ dataToValidate = req.query;
1019
+ break;
1020
+ case "params":
1021
+ dataToValidate = req.params;
1022
+ break;
1023
+ case "all":
1024
+ dataToValidate = { ...req.body, ...req.query, ...req.params };
1025
+ break;
1026
+ default:
1027
+ dataToValidate = req.body;
1028
+ }
1029
+ if (dataToValidate === void 0 || dataToValidate === null) {
1030
+ dataToValidate = {};
1031
+ }
1032
+ const errors = this.validator.validate(dataToValidate);
1033
+ if (errors && typeof errors === "object" && Object.keys(errors).length > 0) {
1034
+ res.status(400).json({
1035
+ message: "Validation failed",
1036
+ errors
1037
+ });
1038
+ return;
1039
+ }
1040
+ next();
1041
+ };
1042
+ }
1043
+ };
1044
+ export {
1045
+ APIGateway,
1046
+ AuthMiddleware,
1047
+ BaseMiddleware,
1048
+ GatewayRoute,
1049
+ HttpClient,
1050
+ LoadBalancer,
1051
+ LoggingMiddleware,
1052
+ MiddlewareChain,
1053
+ RateLimitMiddleware,
1054
+ ServiceRegistry,
1055
+ ValidationMiddleware
1056
+ };