@derian-cordoba/api-gateway 1.0.0 → 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 CHANGED
@@ -12,6 +12,9 @@ A generic, configuration-driven HTTP API gateway. Routes incoming requests to up
12
12
  - [Configuration](#configuration)
13
13
  - [Environment Variables](#environment-variables)
14
14
  - [Route Configuration](#route-configuration)
15
+ - [Authentication](#authentication)
16
+ - [JWT](#jwt)
17
+ - [API Key](#api-key)
15
18
  - [Running the Gateway](#running-the-gateway)
16
19
  - [Health Check](#health-check)
17
20
  - [Project Structure](#project-structure)
@@ -23,6 +26,7 @@ A generic, configuration-driven HTTP API gateway. Routes incoming requests to up
23
26
  ## Features
24
27
 
25
28
  - **Configuration-driven routing** — define proxy routes in a JSON file, an environment variable, or both; changes take effect on restart with zero code changes
29
+ - **Per-route authentication** — protect any route with a JWT Bearer token (HMAC or RSA/EC) or an API key; set `enabled: false` to bypass with zero overhead
26
30
  - **Per-route rate limiting** — each route can declare its own `max` requests / `windowMs` window, enforced by `express-rate-limit`
27
31
  - **Startup validation** — route config is validated with Zod at boot time; the process exits with a descriptive error rather than silently misbehaving
28
32
  - **Structured logging** — `pino` + `pino-http` emit newline-delimited JSON in production and human-readable output (via `pino-pretty`) in development
@@ -98,6 +102,13 @@ Copy `.env.example` to `.env` and edit as needed.
98
102
  | `ROUTES_FILE_PATH` | `routes.json` | Path to the JSON route config file, relative to `process.cwd()`. |
99
103
  | `ROUTES` | _(none)_ | Inline route definitions as a JSON array. Merged with `ROUTES_FILE_PATH`. Useful for containerised deployments where injecting a file is inconvenient. |
100
104
 
105
+ #### Authentication
106
+
107
+ | Variable | Default | Description |
108
+ |---|---|---|
109
+ | `JWT_SECRET` | _(none)_ | Fallback HMAC signing secret used when a JWT route has no inline `secret` field. |
110
+ | `JWT_PUBLIC_KEY` | _(none)_ | Fallback PEM public key used when a JWT route has no inline `publicKey` field. Takes precedence over `JWT_SECRET`. |
111
+
101
112
  ---
102
113
 
103
114
  ### Route Configuration
@@ -135,6 +146,35 @@ Routes are defined as a JSON array. Each entry is a **Gateway** object:
135
146
 
136
147
  Responses include standard `RateLimit-*` headers (RFC draft-8).
137
148
 
149
+ #### `Auth`
150
+
151
+ Adds authentication middleware to a route. When `enabled` is `false` the middleware is a no-op passthrough — no overhead, no token check.
152
+
153
+ Two strategies are supported, selected with the `strategy` field.
154
+
155
+ **`"jwt"` — Bearer token validation**
156
+
157
+ | Field | Type | Required | Description |
158
+ |---|---|---|---|
159
+ | `enabled` | `boolean` | ✅ | `true` to enforce, `false` to bypass. |
160
+ | `strategy` | `"jwt"` | ✅ | — |
161
+ | `secret` | `string` | — | Shared secret for HMAC algorithms (HS256, HS384, HS512). Falls back to `JWT_SECRET` env var. |
162
+ | `publicKey` | `string` | — | PEM-encoded public key or X.509 certificate for asymmetric algorithms (RS256, RS384, RS512, ES256 …). Falls back to `JWT_PUBLIC_KEY` env var. Takes precedence over `secret` when both are present. |
163
+ | `algorithms` | `string[]` | — | Explicit algorithm allowlist. Defaults to `["RS256"]` when `publicKey` is used, `["HS256"]` otherwise. Recommended to prevent algorithm-confusion attacks. |
164
+
165
+ The gateway expects the token in the `Authorization: Bearer <token>` header and returns `401` on a missing, malformed, or invalid token.
166
+
167
+ **`"apiKey"` — Header-based API key**
168
+
169
+ | Field | Type | Required | Description |
170
+ |---|---|---|---|
171
+ | `enabled` | `boolean` | ✅ | `true` to enforce, `false` to bypass. |
172
+ | `strategy` | `"apiKey"` | ✅ | — |
173
+ | `keys` | `string[]` | ✅ | List of valid API keys. At least one entry required. |
174
+ | `header` | `string` | — | Header name to read the key from (default: `x-api-key`). |
175
+
176
+ Returns `401` when the header is absent or the value is not in `keys`.
177
+
138
178
  #### Example `routes.json`
139
179
 
140
180
  ```json
@@ -158,11 +198,24 @@ Responses include standard `RateLimit-*` headers (RFC draft-8).
158
198
  "proxy": {
159
199
  "target": "http://orders-service:3002",
160
200
  "changeOrigin": true,
161
- "pathRewrite": { "^/orders": "" },
162
- "headers": {
163
- "X-Internal-Source": "api-gateway"
164
- },
165
- "timeout": 5000
201
+ "pathRewrite": { "^/orders": "" }
202
+ },
203
+ "auth": {
204
+ "enabled": true,
205
+ "strategy": "jwt"
206
+ }
207
+ },
208
+ {
209
+ "baseURL": "/reports",
210
+ "proxy": {
211
+ "target": "http://reports-service:3003",
212
+ "changeOrigin": true,
213
+ "pathRewrite": { "^/reports": "" }
214
+ },
215
+ "auth": {
216
+ "enabled": true,
217
+ "strategy": "apiKey",
218
+ "keys": ["key-service-alpha-123", "key-service-beta-456"]
166
219
  }
167
220
  }
168
221
  ]
@@ -177,6 +230,93 @@ Routes are loaded from two sources and **merged**:
177
230
 
178
231
  ---
179
232
 
233
+ ## Authentication
234
+
235
+ Authentication is optional and configured per route via the `auth` field. The middleware is applied before rate limiting and proxying. When `enabled: false` the handler is a single no-op function — zero overhead on unprotected routes.
236
+
237
+ ### JWT
238
+
239
+ Protect a route with a Bearer token. The gateway validates the token signature; your upstream receives the request only if verification passes.
240
+
241
+ ```json
242
+ {
243
+ "baseURL": "/orders",
244
+ "proxy": { "target": "http://orders-service:3002", "changeOrigin": true },
245
+ "auth": {
246
+ "enabled": true,
247
+ "strategy": "jwt"
248
+ }
249
+ }
250
+ ```
251
+
252
+ The signing key is resolved in this order:
253
+
254
+ 1. `publicKey` field in the route config (PEM — use for RS256 / ES256)
255
+ 2. `JWT_PUBLIC_KEY` environment variable
256
+ 3. `secret` field in the route config (string — use for HS256)
257
+ 4. `JWT_SECRET` environment variable
258
+
259
+ `publicKey` always takes precedence over `secret`. If neither is present the gateway returns `401`.
260
+
261
+ **HMAC (HS256) — shared secret:**
262
+
263
+ ```bash
264
+ # .env
265
+ JWT_SECRET=super-secret-key-change-in-production
266
+ ```
267
+
268
+ ```bash
269
+ TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
270
+ -H "Content-Type: application/json" \
271
+ -d '{"username":"alice","password":"password123"}' | jq -r '.token')
272
+
273
+ curl http://localhost:3000/orders \
274
+ -H "Authorization: Bearer $TOKEN"
275
+ ```
276
+
277
+ **RSA (RS256) — public/private key pair:**
278
+
279
+ ```json
280
+ {
281
+ "auth": {
282
+ "enabled": true,
283
+ "strategy": "jwt",
284
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----",
285
+ "algorithms": ["RS256"]
286
+ }
287
+ }
288
+ ```
289
+
290
+ ### API Key
291
+
292
+ Protect a route with a pre-shared key delivered in a request header.
293
+
294
+ ```json
295
+ {
296
+ "baseURL": "/reports",
297
+ "proxy": { "target": "http://reports-service:3003", "changeOrigin": true },
298
+ "auth": {
299
+ "enabled": true,
300
+ "strategy": "apiKey",
301
+ "header": "x-api-key",
302
+ "keys": ["key-service-alpha-123", "key-service-beta-456"]
303
+ }
304
+ }
305
+ ```
306
+
307
+ ```bash
308
+ # Valid key → 200
309
+ curl http://localhost:3000/reports \
310
+ -H "x-api-key: key-service-alpha-123"
311
+
312
+ # Missing or wrong key → 401
313
+ curl http://localhost:3000/reports
314
+ ```
315
+
316
+ Multiple keys in `keys` let you rotate credentials without downtime — add the new key, deploy, then remove the old one.
317
+
318
+ ---
319
+
180
320
  ## Running the Gateway
181
321
 
182
322
  ### Development (hot-reload)
@@ -245,88 +385,98 @@ src/apps/api-gateway/
245
385
  │ ├── env/config.ts # NODE_ENV → isDev flag
246
386
  │ ├── gateway/config.ts # GATEWAY_PORT, GATEWAY_PREFIX
247
387
  │ ├── cors/config.ts # CORS_ORIGINS, CORS_METHODS, CORS_HEADERS
248
- └── routes/config.ts # ROUTES_FILE_PATH
388
+ ├── routes/config.ts # ROUTES_FILE_PATH
389
+ │ └── auth/config.ts # JWT_SECRET, JWT_PUBLIC_KEY
390
+
391
+ ├── middleware/
392
+ │ ├── authMiddleware.ts # Factory — returns the right strategy or a no-op
393
+ │ └── auth/
394
+ │ ├── AuthStrategy.ts # Interface (Strategy pattern)
395
+ │ ├── JwtAuthStrategy.ts # JWT Bearer token validation (HMAC + RSA/EC)
396
+ │ └── ApiKeyAuthStrategy.ts # Header-based API key validation
249
397
 
250
398
  ├── routes/
251
399
  │ ├── Router.ts # Middleware pipeline (logging → security → CORS → body → proxy → errors)
252
400
  │ ├── ProxyManager.ts # Reads, validates, and registers proxy routes
253
- │ ├── RouteValidator.ts # Zod schemas for Gateway / Proxy / RateLimit types
401
+ │ ├── RouteValidator.ts # Zod schemas for Gateway / Proxy / RateLimit / Auth types
254
402
  │ └── HealthRouter.ts # GET /health handler
255
403
 
256
404
  └── types/
257
405
  ├── gateway.d.ts # Gateway type
258
406
  ├── proxy.d.ts # Proxy type
259
- └── rate-limit.d.ts # RateLimit type
407
+ ├── rate-limit.d.ts # RateLimit type
408
+ └── auth.d.ts # Auth type (JwtAuth | ApiKeyAuth)
260
409
  ```
261
410
 
262
411
  ---
263
412
 
264
413
  ## Example Project
265
414
 
266
- `examples/basic/` contains a self-contained demo with two mock upstream services and a pre-built gateway config.
267
-
268
- ### What's included
269
-
270
- | File | Description |
271
- |---|---|
272
- | `routes.json` | Gateway config with two routes (`/users`, `/products`) each with rate limiting and path rewriting |
273
- | `.env` | Gateway environment for the example |
274
- | `upstream-users.js` | Mock Users service on port `4001` |
275
- | `upstream-products.js` | Mock Products service on port `4002` |
276
- | `run.sh` | Starts all three processes and stops them together on Ctrl+C |
277
-
278
- ### Running the example
415
+ `examples/` contains a self-contained demo that starts five upstream services and one gateway covering all features: rate limiting, JWT auth, and API key auth.
279
416
 
280
417
  ```bash
281
418
  pnpm example
282
419
  ```
283
420
 
284
- This copies `examples/basic/.env` to the project root and starts all three services. Once running:
285
-
286
- | Endpoint | Description |
287
- |---|---|
288
- | `http://localhost:3000/health` | Gateway health check |
289
- | `http://localhost:3000/users` | Proxied to Users service |
290
- | `http://localhost:3000/products` | Proxied to Products service |
291
-
292
- ### Users service endpoints (`/users`)
293
-
294
- | Method | Path | Description |
295
- |---|---|---|
296
- | `GET` | `/users` | List all users |
297
- | `GET` | `/users/:id` | Get a user by ID |
298
- | `POST` | `/users` | Create a user (body: `{ name, email }`) |
299
- | `DELETE` | `/users/:id` | Delete a user by ID |
300
-
301
- ### Products service endpoints (`/products`)
421
+ This copies `examples/.env` to the project root and starts everything. Once running:
302
422
 
303
- | Method | Path | Description |
423
+ | Endpoint | Auth | Description |
304
424
  |---|---|---|
305
- | `GET` | `/products` | List all products |
306
- | `GET` | `/products/:id` | Get a product by ID |
307
- | `POST` | `/products` | Create a product (body: `{ name, price }`) |
308
- | `PATCH` | `/products/:id/stock` | Adjust stock (body: `{ quantity: number }`) |
425
+ | `http://localhost:3000/health` | | Gateway health check |
426
+ | `http://localhost:3000/users` | | Users service (rate limited) |
427
+ | `http://localhost:3000/products` | | Products service (rate limited) |
428
+ | `http://localhost:3000/auth/login` | | Issues JWT tokens |
429
+ | `http://localhost:3000/orders` | JWT Bearer | Orders service |
430
+ | `http://localhost:3000/reports` | API key | Reports service |
309
431
 
310
- ### Example requests
432
+ ### Public routes
311
433
 
312
434
  ```bash
313
- # List users
314
435
  curl http://localhost:3000/users
436
+ curl http://localhost:3000/products/1
437
+ ```
438
+
439
+ ### JWT-protected route (`/orders`)
315
440
 
316
- # Create a user
317
- curl -X POST http://localhost:3000/users \
441
+ ```bash
442
+ # 1. Log in to get a token (users: alice/password123, bob/password456)
443
+ TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
318
444
  -H "Content-Type: application/json" \
319
- -d '{"name": "Alice", "email": "alice@example.com"}'
445
+ -d '{"username":"alice","password":"password123"}' | jq -r '.token')
320
446
 
321
- # Get a product
322
- curl http://localhost:3000/products/1
447
+ # 2. Access the protected route
448
+ curl http://localhost:3000/orders \
449
+ -H "Authorization: Bearer $TOKEN"
323
450
 
324
- # Update product stock
325
- curl -X PATCH http://localhost:3000/products/1/stock \
451
+ # 3. Create an order
452
+ curl -X POST http://localhost:3000/orders \
453
+ -H "Authorization: Bearer $TOKEN" \
326
454
  -H "Content-Type: application/json" \
327
- -d '{"quantity": 25}'
455
+ -d '{"userId":1,"items":[{"productId":2,"qty":1}],"total":24.99}'
328
456
  ```
329
457
 
458
+ ### API-key-protected route (`/reports`)
459
+
460
+ ```bash
461
+ # Valid keys: key-service-alpha-123 · key-service-beta-456
462
+
463
+ curl http://localhost:3000/reports \
464
+ -H "x-api-key: key-service-alpha-123"
465
+
466
+ curl http://localhost:3000/reports/sales \
467
+ -H "x-api-key: key-service-beta-456"
468
+ ```
469
+
470
+ ### Individual example directories
471
+
472
+ Each sub-directory is also usable as a standalone reference:
473
+
474
+ | Directory | Description |
475
+ |---|---|
476
+ | `examples/basic/` | Rate limiting only — Users + Products services |
477
+ | `examples/jwt-auth/` | JWT auth — Auth service + Orders service |
478
+ | `examples/api-key-auth/` | API key auth — Reports service |
479
+
330
480
  ---
331
481
 
332
482
  ## Architecture
@@ -339,18 +489,19 @@ Client
339
489
 
340
490
  Express app
341
491
 
342
- ├─ pino-http structured request/response logging
343
- ├─ helmet security headers (CSP, HSTS, X-Frame-Options, …)
344
- ├─ cors configurable origin / method / header policy
345
- ├─ express.json body parsing
346
- ├─ compression gzip response compression
492
+ ├─ pino-http structured request/response logging
493
+ ├─ helmet security headers (CSP, HSTS, X-Frame-Options, …)
494
+ ├─ cors configurable origin / method / header policy
495
+ ├─ express.json body parsing
496
+ ├─ compression gzip response compression
347
497
 
348
- ├─ GET /health health check — short-circuits here
498
+ ├─ GET /health health check — short-circuits here
349
499
 
350
- ├─ express-rate-limit per-route request throttling (applied per baseURL)
500
+ ├─ authMiddleware per-route JWT or API key check → 401 on failure
501
+ ├─ express-rate-limit per-route request throttling → 429 on exceeded
351
502
  ├─ http-proxy-middleware proxies request to upstream, forwards body
352
503
 
353
- └─ error handler catches unhandled errors → 500 JSON response
504
+ └─ error handler catches unhandled errors → 500 JSON response
354
505
  ```
355
506
 
356
507
  ### Startup sequence
@@ -2,10 +2,12 @@ import { type GatewayConfig } from "./gateway/config";
2
2
  import { type CorsConfig } from "./cors/config";
3
3
  import { type RoutesConfig } from "./routes/config";
4
4
  import { type EnvConfig } from "./env/config";
5
+ import { type AuthConfig } from "./auth/config";
5
6
  export type AppEnv = {
6
7
  env: EnvConfig;
7
8
  gateway: GatewayConfig;
8
9
  cors: CorsConfig;
9
10
  routes: RoutesConfig;
11
+ auth: AuthConfig;
10
12
  };
11
13
  export declare const appEnv: AppEnv;
@@ -5,9 +5,11 @@ const config_1 = require("./gateway/config");
5
5
  const config_2 = require("./cors/config");
6
6
  const config_3 = require("./routes/config");
7
7
  const config_4 = require("./env/config");
8
+ const config_5 = require("./auth/config");
8
9
  exports.appEnv = {
9
10
  env: config_4.envConfig,
10
11
  gateway: config_1.gatewayConfig,
11
12
  cors: config_2.corsConfig,
12
13
  routes: config_3.routesConfig,
14
+ auth: config_5.authConfig,
13
15
  };
@@ -0,0 +1,5 @@
1
+ export type AuthConfig = {
2
+ jwtSecret: string | undefined;
3
+ jwtPublicKey: string | undefined;
4
+ };
5
+ export declare const authConfig: AuthConfig;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authConfig = void 0;
4
+ const { JWT_SECRET, JWT_PUBLIC_KEY, } = process.env;
5
+ exports.authConfig = {
6
+ jwtSecret: JWT_SECRET,
7
+ jwtPublicKey: JWT_PUBLIC_KEY,
8
+ };
@@ -0,0 +1,9 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ import type { ApiKeyAuth } from "../../types/auth";
3
+ import type { AuthStrategy } from "./AuthStrategy";
4
+ export declare class ApiKeyAuthStrategy implements AuthStrategy {
5
+ private readonly auth;
6
+ private readonly headerName;
7
+ constructor(auth: ApiKeyAuth);
8
+ handle(req: Request, res: Response, next: NextFunction): void;
9
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiKeyAuthStrategy = void 0;
4
+ const http_status_codes_1 = require("http-status-codes");
5
+ class ApiKeyAuthStrategy {
6
+ constructor(auth) {
7
+ var _a;
8
+ this.auth = auth;
9
+ this.headerName = ((_a = auth.header) !== null && _a !== void 0 ? _a : "x-api-key").toLowerCase();
10
+ }
11
+ handle(req, res, next) {
12
+ const provided = req.headers[this.headerName];
13
+ if (typeof provided !== "string" || !this.auth.keys.includes(provided)) {
14
+ res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "Invalid or missing API key" });
15
+ return;
16
+ }
17
+ next();
18
+ }
19
+ }
20
+ exports.ApiKeyAuthStrategy = ApiKeyAuthStrategy;
@@ -0,0 +1,4 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ export interface AuthStrategy {
3
+ handle(req: Request, res: Response, next: NextFunction): void;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ import type { JwtAuth } from "../../types/auth";
3
+ import type { AuthStrategy } from "./AuthStrategy";
4
+ export declare class JwtAuthStrategy implements AuthStrategy {
5
+ private readonly auth;
6
+ constructor(auth: JwtAuth);
7
+ handle(req: Request, res: Response, next: NextFunction): void;
8
+ private extractToken;
9
+ private resolveKey;
10
+ private resolveAlgorithms;
11
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JwtAuthStrategy = void 0;
7
+ const http_status_codes_1 = require("http-status-codes");
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ const app_env_1 = require("../../config/app-env");
10
+ class JwtAuthStrategy {
11
+ constructor(auth) {
12
+ this.auth = auth;
13
+ }
14
+ handle(req, res, next) {
15
+ const token = this.extractToken(req);
16
+ if (!token) {
17
+ res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "Missing or malformed Authorization header" });
18
+ return;
19
+ }
20
+ const resolved = this.resolveKey();
21
+ if (!resolved) {
22
+ res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "JWT key not configured" });
23
+ return;
24
+ }
25
+ try {
26
+ jsonwebtoken_1.default.verify(token, resolved.key, { algorithms: this.resolveAlgorithms(resolved.isAsymmetric) });
27
+ next();
28
+ }
29
+ catch (_a) {
30
+ res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "Invalid or expired token" });
31
+ }
32
+ }
33
+ extractToken(req) {
34
+ const { authorization } = req.headers;
35
+ if (!(authorization === null || authorization === void 0 ? void 0 : authorization.startsWith("Bearer ")))
36
+ return null;
37
+ return authorization.slice(7);
38
+ }
39
+ resolveKey() {
40
+ var _a, _b;
41
+ const publicKey = (_a = this.auth.publicKey) !== null && _a !== void 0 ? _a : app_env_1.appEnv.auth.jwtPublicKey;
42
+ if (publicKey)
43
+ return { key: publicKey, isAsymmetric: true };
44
+ const secret = (_b = this.auth.secret) !== null && _b !== void 0 ? _b : app_env_1.appEnv.auth.jwtSecret;
45
+ if (secret)
46
+ return { key: secret, isAsymmetric: false };
47
+ return null;
48
+ }
49
+ resolveAlgorithms(isAsymmetric) {
50
+ if (this.auth.algorithms)
51
+ return this.auth.algorithms;
52
+ return isAsymmetric ? ["RS256"] : ["HS256"];
53
+ }
54
+ }
55
+ exports.JwtAuthStrategy = JwtAuthStrategy;
@@ -0,0 +1,3 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { Auth } from "../types/auth";
3
+ export declare function createAuthMiddleware(auth: Auth): RequestHandler;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAuthMiddleware = createAuthMiddleware;
4
+ const JwtAuthStrategy_1 = require("./auth/JwtAuthStrategy");
5
+ const ApiKeyAuthStrategy_1 = require("./auth/ApiKeyAuthStrategy");
6
+ const strategyFactories = {
7
+ jwt: (auth) => new JwtAuthStrategy_1.JwtAuthStrategy(auth),
8
+ apiKey: (auth) => new ApiKeyAuthStrategy_1.ApiKeyAuthStrategy(auth),
9
+ };
10
+ function createAuthMiddleware(auth) {
11
+ if (!auth.enabled) {
12
+ return (_req, _res, next) => next();
13
+ }
14
+ const strategy = strategyFactories[auth.strategy](auth);
15
+ return (req, res, next) => strategy.handle(req, res, next);
16
+ }
@@ -18,6 +18,7 @@ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
18
18
  const http_status_codes_1 = require("http-status-codes");
19
19
  const promises_1 = require("node:fs/promises");
20
20
  const RouteValidator_1 = require("./RouteValidator");
21
+ const authMiddleware_1 = require("../middleware/authMiddleware");
21
22
  const logger_1 = require("../logger");
22
23
  const app_env_1 = require("../config/app-env");
23
24
  class ProxyManager {
@@ -37,6 +38,10 @@ class ProxyManager {
37
38
  }
38
39
  routes.forEach((route) => {
39
40
  var _a, _b;
41
+ // Apply per-route authentication when configured
42
+ if (route.auth) {
43
+ this.router.use(route.baseURL, (0, authMiddleware_1.createAuthMiddleware)(route.auth));
44
+ }
40
45
  // Apply per-route rate limiting when configured
41
46
  if (route.rateLimit) {
42
47
  this.router.use(route.baseURL, (0, express_rate_limit_1.default)({
@@ -17,5 +17,17 @@ export declare const GatewaysSchema: z.ZodArray<z.ZodObject<{
17
17
  statusCode: z.ZodOptional<z.ZodNumber>;
18
18
  message: z.ZodOptional<z.ZodString>;
19
19
  }, z.core.$strip>>;
20
+ auth: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
21
+ enabled: z.ZodBoolean;
22
+ strategy: z.ZodLiteral<"jwt">;
23
+ secret: z.ZodOptional<z.ZodString>;
24
+ publicKey: z.ZodOptional<z.ZodString>;
25
+ algorithms: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
+ }, z.core.$strip>, z.ZodObject<{
27
+ enabled: z.ZodBoolean;
28
+ strategy: z.ZodLiteral<"apiKey">;
29
+ header: z.ZodOptional<z.ZodString>;
30
+ keys: z.ZodArray<z.ZodString>;
31
+ }, z.core.$strip>], "strategy">>;
20
32
  }, z.core.$strip>>;
21
33
  export declare function validateRoutes(data: unknown): Gateway[];
@@ -18,10 +18,25 @@ const RateLimitSchema = zod_1.z.object({
18
18
  statusCode: zod_1.z.number().optional(),
19
19
  message: zod_1.z.string().optional(),
20
20
  });
21
+ const JwtAuthSchema = zod_1.z.object({
22
+ enabled: zod_1.z.boolean(),
23
+ strategy: zod_1.z.literal("jwt"),
24
+ secret: zod_1.z.string().optional(),
25
+ publicKey: zod_1.z.string().optional(),
26
+ algorithms: zod_1.z.array(zod_1.z.string()).optional(),
27
+ });
28
+ const ApiKeyAuthSchema = zod_1.z.object({
29
+ enabled: zod_1.z.boolean(),
30
+ strategy: zod_1.z.literal("apiKey"),
31
+ header: zod_1.z.string().optional(),
32
+ keys: zod_1.z.array(zod_1.z.string()).min(1, "apiKey auth requires at least one key"),
33
+ });
34
+ const AuthSchema = zod_1.z.discriminatedUnion("strategy", [JwtAuthSchema, ApiKeyAuthSchema]);
21
35
  const GatewaySchema = zod_1.z.object({
22
36
  baseURL: zod_1.z.string().startsWith("/", "baseURL must start with /"),
23
37
  proxy: ProxySchema,
24
38
  rateLimit: RateLimitSchema.optional(),
39
+ auth: AuthSchema.optional(),
25
40
  });
26
41
  exports.GatewaysSchema = zod_1.z.array(GatewaySchema);
27
42
  function validateRoutes(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@derian-cordoba/api-gateway",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A generic, configuration-driven HTTP API gateway with per-route rate limiting, structured logging, and security headers",
5
5
  "main": "./dist/src/apps/api-gateway/index.js",
6
6
  "types": "./dist/src/apps/api-gateway/index.d.ts",
@@ -37,12 +37,13 @@
37
37
  "prepare": "pnpm build",
38
38
  "test": "vitest run",
39
39
  "test:watch": "vitest",
40
- "example": "bash examples/basic/run.sh"
40
+ "example": "bash examples/run.sh"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/compression": "^1.8.0",
44
44
  "@types/cors": "^2.8.18",
45
45
  "@types/express": "^5.0.2",
46
+ "@types/jsonwebtoken": "^9.0.10",
46
47
  "@types/node": "^22.15.21",
47
48
  "@types/supertest": "^7.2.0",
48
49
  "pino-pretty": "^13.1.3",
@@ -60,6 +61,7 @@
60
61
  "helmet": "^8.1.0",
61
62
  "http-proxy-middleware": "^3.0.5",
62
63
  "http-status-codes": "^2.3.0",
64
+ "jsonwebtoken": "^9.0.3",
63
65
  "pino": "^10.3.1",
64
66
  "pino-http": "^11.0.0",
65
67
  "zod": "^4.4.3"