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