@tumbaland/backend-core 1.16.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/.versionrc.json +7 -0
- package/README.md +179 -0
- package/__mocks__/uuid.js +8 -0
- package/dist/app/createBaseApp.d.ts +44 -0
- package/dist/app/createBaseApp.d.ts.map +1 -0
- package/dist/app/createBaseApp.js +54 -0
- package/dist/app/createBaseApp.js.map +1 -0
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +17 -0
- package/dist/config/env.js.map +1 -0
- package/dist/database/connection.d.ts +3 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +49 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/errors/HttpError.d.ts +32 -0
- package/dist/errors/HttpError.d.ts.map +1 -0
- package/dist/errors/HttpError.js +61 -0
- package/dist/errors/HttpError.js.map +1 -0
- package/dist/health/healthController.d.ts +21 -0
- package/dist/health/healthController.d.ts.map +1 -0
- package/dist/health/healthController.js +56 -0
- package/dist/health/healthController.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.d.ts +4 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +78 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/metrics/index.d.ts +17 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +99 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/middleware/authMiddleware.d.ts +9 -0
- package/dist/middleware/authMiddleware.d.ts.map +1 -0
- package/dist/middleware/authMiddleware.js +32 -0
- package/dist/middleware/authMiddleware.js.map +1 -0
- package/dist/middleware/corsMiddleware.d.ts +20 -0
- package/dist/middleware/corsMiddleware.d.ts.map +1 -0
- package/dist/middleware/corsMiddleware.js +33 -0
- package/dist/middleware/corsMiddleware.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +14 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +88 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/requestLogger.d.ts +16 -0
- package/dist/middleware/requestLogger.d.ts.map +1 -0
- package/dist/middleware/requestLogger.js +49 -0
- package/dist/middleware/requestLogger.js.map +1 -0
- package/dist/middleware/security.d.ts +26 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +47 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/middleware/validate.d.ts +11 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +24 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/tracing/index.d.ts +13 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +89 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/types/auth.d.ts +32 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +3 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/utils/correlation.d.ts +11 -0
- package/dist/utils/correlation.d.ts.map +1 -0
- package/dist/utils/correlation.js +23 -0
- package/dist/utils/correlation.js.map +1 -0
- package/dist/utils/response.d.ts +24 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +35 -0
- package/dist/utils/response.js.map +1 -0
- package/jest.config.js +14 -0
- package/package.json +61 -0
- package/src/app/createBaseApp.test.ts +69 -0
- package/src/app/createBaseApp.ts +75 -0
- package/src/config/env.test.ts +28 -0
- package/src/config/env.ts +13 -0
- package/src/database/connection.test.ts +142 -0
- package/src/database/connection.ts +46 -0
- package/src/errors/HttpError.test.ts +38 -0
- package/src/errors/HttpError.ts +57 -0
- package/src/health/healthController.test.ts +91 -0
- package/src/health/healthController.ts +56 -0
- package/src/index.ts +60 -0
- package/src/logging/logger.test.ts +76 -0
- package/src/logging/logger.ts +103 -0
- package/src/metrics/index.test.ts +91 -0
- package/src/metrics/index.ts +110 -0
- package/src/middleware/authMiddleware.test.ts +104 -0
- package/src/middleware/authMiddleware.ts +29 -0
- package/src/middleware/corsMiddleware.test.ts +58 -0
- package/src/middleware/corsMiddleware.ts +36 -0
- package/src/middleware/errorHandler.test.ts +146 -0
- package/src/middleware/errorHandler.ts +98 -0
- package/src/middleware/requestLogger.test.ts +81 -0
- package/src/middleware/requestLogger.ts +47 -0
- package/src/middleware/security.test.ts +45 -0
- package/src/middleware/security.ts +43 -0
- package/src/middleware/validate.test.ts +60 -0
- package/src/middleware/validate.ts +23 -0
- package/src/tracing/index.test.ts +250 -0
- package/src/tracing/index.ts +97 -0
- package/src/types/auth.ts +33 -0
- package/src/utils/correlation.test.ts +47 -0
- package/src/utils/correlation.ts +22 -0
- package/src/utils/response.test.ts +64 -0
- package/src/utils/response.ts +60 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Options as RateLimitOptions } from 'express-rate-limit';
|
|
2
|
+
/**
|
|
3
|
+
* Standard security headers (helmet) for all services.
|
|
4
|
+
*
|
|
5
|
+
* - `contentSecurityPolicy` is off: these are JSON APIs, not HTML apps, and a
|
|
6
|
+
* couple of services serve a static Swagger docs page under `/api/docs`
|
|
7
|
+
* whose inline scripts a default CSP would break.
|
|
8
|
+
* - `crossOriginResourcePolicy` is relaxed to `cross-origin`: helmet's default
|
|
9
|
+
* (`same-origin`) makes browsers refuse to read the response from a
|
|
10
|
+
* different origin — which every frontend is, since each front and backend
|
|
11
|
+
* service runs on its own port. CORS (see `createCorsMiddleware`) already
|
|
12
|
+
* does origin enforcement; this header must not fight it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const securityHeaders: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, next: (err?: unknown) => void) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Build a rate limiter with sane JSON error output. Counts per client IP.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createRateLimiter(options?: Partial<RateLimitOptions>): import("express-rate-limit").RateLimitRequestHandler;
|
|
19
|
+
/** Baseline limiter for authenticated/general routes. */
|
|
20
|
+
export declare const standardRateLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
21
|
+
/**
|
|
22
|
+
* Tighter limiter for brute-force-prone unauthenticated routes (token
|
|
23
|
+
* issuance, public endpoints with no auth in front of them).
|
|
24
|
+
*/
|
|
25
|
+
export declare const strictRateLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
26
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/middleware/security.ts"],"names":[],"mappings":"AACA,OAAkB,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE5E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,gGA4Bi/H,CAAC,2BAxB5gI,CAAC;AAEH;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,OAAO,CAAC,gBAAgB,CAAM,wDASxE;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,sDAAsB,CAAC;AAEvD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,sDAAiC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
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.strictRateLimiter = exports.standardRateLimiter = exports.securityHeaders = void 0;
|
|
7
|
+
exports.createRateLimiter = createRateLimiter;
|
|
8
|
+
const helmet_1 = __importDefault(require("helmet"));
|
|
9
|
+
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
10
|
+
/**
|
|
11
|
+
* Standard security headers (helmet) for all services.
|
|
12
|
+
*
|
|
13
|
+
* - `contentSecurityPolicy` is off: these are JSON APIs, not HTML apps, and a
|
|
14
|
+
* couple of services serve a static Swagger docs page under `/api/docs`
|
|
15
|
+
* whose inline scripts a default CSP would break.
|
|
16
|
+
* - `crossOriginResourcePolicy` is relaxed to `cross-origin`: helmet's default
|
|
17
|
+
* (`same-origin`) makes browsers refuse to read the response from a
|
|
18
|
+
* different origin — which every frontend is, since each front and backend
|
|
19
|
+
* service runs on its own port. CORS (see `createCorsMiddleware`) already
|
|
20
|
+
* does origin enforcement; this header must not fight it.
|
|
21
|
+
*/
|
|
22
|
+
exports.securityHeaders = (0, helmet_1.default)({
|
|
23
|
+
contentSecurityPolicy: false,
|
|
24
|
+
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
25
|
+
crossOriginEmbedderPolicy: false
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Build a rate limiter with sane JSON error output. Counts per client IP.
|
|
29
|
+
*/
|
|
30
|
+
function createRateLimiter(options = {}) {
|
|
31
|
+
return (0, express_rate_limit_1.default)({
|
|
32
|
+
windowMs: 15 * 60 * 1000,
|
|
33
|
+
max: 300,
|
|
34
|
+
standardHeaders: true,
|
|
35
|
+
legacyHeaders: false,
|
|
36
|
+
message: { success: false, message: 'Too many requests, please try again later' },
|
|
37
|
+
...options
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Baseline limiter for authenticated/general routes. */
|
|
41
|
+
exports.standardRateLimiter = createRateLimiter();
|
|
42
|
+
/**
|
|
43
|
+
* Tighter limiter for brute-force-prone unauthenticated routes (token
|
|
44
|
+
* issuance, public endpoints with no auth in front of them).
|
|
45
|
+
*/
|
|
46
|
+
exports.strictRateLimiter = createRateLimiter({ max: 20 });
|
|
47
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/middleware/security.ts"],"names":[],"mappings":";;;;;;AAwBA,8CASC;AAjCD,oDAA4B;AAC5B,4EAA4E;AAE5E;;;;;;;;;;;GAWG;AACU,QAAA,eAAe,GAAG,IAAA,gBAAM,EAAC;IACpC,qBAAqB,EAAE,KAAK;IAC5B,yBAAyB,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE;IACrD,yBAAyB,EAAE,KAAK;CACjC,CAAC,CAAC;AAEH;;GAEG;AACH,SAAgB,iBAAiB,CAAC,UAAqC,EAAE;IACvE,OAAO,IAAA,4BAAS,EAAC;QACf,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;QACxB,GAAG,EAAE,GAAG;QACR,eAAe,EAAE,IAAI;QACrB,aAAa,EAAE,KAAK;QACpB,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,2CAA2C,EAAE;QACjF,GAAG,OAAO;KACX,CAAC,CAAC;AACL,CAAC;AAED,yDAAyD;AAC5C,QAAA,mBAAmB,GAAG,iBAAiB,EAAE,CAAC;AAEvD;;;GAGG;AACU,QAAA,iBAAiB,GAAG,iBAAiB,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RequestHandler } from 'express';
|
|
2
|
+
import { ZodType } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Validates `req.body` against a zod schema before the route handler runs.
|
|
5
|
+
* On failure, throws a `BadRequestError` (caught by the shared errorHandler)
|
|
6
|
+
* instead of letting controllers hand-roll their own field checks. On
|
|
7
|
+
* success, `req.body` is replaced with the parsed result so handlers get
|
|
8
|
+
* zod's inferred, coerced types rather than raw untyped input.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validate(schema: ZodType): RequestHandler;
|
|
11
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/middleware/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAG9B;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,cAAc,CAWxD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validate = validate;
|
|
4
|
+
const HttpError_1 = require("../errors/HttpError");
|
|
5
|
+
/**
|
|
6
|
+
* Validates `req.body` against a zod schema before the route handler runs.
|
|
7
|
+
* On failure, throws a `BadRequestError` (caught by the shared errorHandler)
|
|
8
|
+
* instead of letting controllers hand-roll their own field checks. On
|
|
9
|
+
* success, `req.body` is replaced with the parsed result so handlers get
|
|
10
|
+
* zod's inferred, coerced types rather than raw untyped input.
|
|
11
|
+
*/
|
|
12
|
+
function validate(schema) {
|
|
13
|
+
return (req, _res, next) => {
|
|
14
|
+
const result = schema.safeParse(req.body);
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
const issue = result.error.issues[0];
|
|
17
|
+
const field = issue.path.length ? `${issue.path.join('.')}: ` : '';
|
|
18
|
+
throw new HttpError_1.BadRequestError(`${field}${issue.message}`);
|
|
19
|
+
}
|
|
20
|
+
req.body = result.data;
|
|
21
|
+
next();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/middleware/validate.ts"],"names":[],"mappings":";;AAWA,4BAWC;AApBD,mDAAsD;AAEtD;;;;;;GAMG;AACH,SAAgB,QAAQ,CAAC,MAAe;IACtC,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAQ,EAAE;QAChE,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,MAAM,IAAI,2BAAe,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACvB,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Tracer } from '@opentelemetry/api';
|
|
2
|
+
export declare const initTracer: (serviceName: string) => Tracer;
|
|
3
|
+
export declare const getTracer: () => Tracer;
|
|
4
|
+
export declare const startSpan: (operationName: string, parentSpan?: any) => import("@opentelemetry/api").Span;
|
|
5
|
+
export declare const tracingMiddleware: (req: any, res: any, next: any) => void;
|
|
6
|
+
export declare const createChildSpan: (operationName: string, parentSpan: any) => import("@opentelemetry/api").Span;
|
|
7
|
+
export declare const logToSpan: (span: any, event: string, data?: any) => void;
|
|
8
|
+
export declare const setSpanTag: (span: any, key: string, value: any) => void;
|
|
9
|
+
export declare const injectHeaders: (span: any) => {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const extractSpanContext: (headers: any) => import("@opentelemetry/api").Context;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tracing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyC,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAQnF,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM,KAAG,MAiBhD,CAAC;AAKF,eAAO,MAAM,SAAS,QAAO,MAK5B,CAAC;AAGF,eAAO,MAAM,SAAS,GAAI,eAAe,MAAM,EAAE,aAAa,GAAG,sCAShE,CAAC;AAGF,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,SAmB9D,CAAC;AAGF,eAAO,MAAM,eAAe,GAAI,eAAe,MAAM,EAAE,YAAY,GAAG,sCAErE,CAAC;AAGF,eAAO,MAAM,SAAS,GAAI,MAAM,GAAG,EAAE,OAAO,MAAM,EAAE,OAAO,GAAG,SAE7D,CAAC;AAGF,eAAO,MAAM,UAAU,GAAI,MAAM,GAAG,EAAE,KAAK,MAAM,EAAE,OAAO,GAAG,SAE5D,CAAC;AAGF,eAAO,MAAM,aAAa,GAAI,MAAM,GAAG;;CAItC,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAI,SAAS,GAAG,yCAE9C,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractSpanContext = exports.injectHeaders = exports.setSpanTag = exports.logToSpan = exports.createChildSpan = exports.tracingMiddleware = exports.startSpan = exports.getTracer = exports.initTracer = void 0;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
|
|
6
|
+
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
7
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
8
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
9
|
+
// Initialize OpenTelemetry tracer provider, exporting via OTLP/HTTP.
|
|
10
|
+
// Endpoint resolution: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT > JAEGER_ENDPOINT > in-cluster collector default.
|
|
11
|
+
const initTracer = (serviceName) => {
|
|
12
|
+
const url = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
|
|
13
|
+
process.env.JAEGER_ENDPOINT ||
|
|
14
|
+
'http://jaeger-collector:4318/v1/traces';
|
|
15
|
+
const provider = new sdk_trace_node_1.NodeTracerProvider({
|
|
16
|
+
resource: (0, resources_1.resourceFromAttributes)({
|
|
17
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: serviceName,
|
|
18
|
+
[semantic_conventions_1.ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0',
|
|
19
|
+
'deployment.environment': process.env.NODE_ENV || 'development',
|
|
20
|
+
}),
|
|
21
|
+
spanProcessors: [new sdk_trace_node_1.BatchSpanProcessor(new exporter_trace_otlp_http_1.OTLPTraceExporter({ url }))],
|
|
22
|
+
});
|
|
23
|
+
provider.register();
|
|
24
|
+
return provider.getTracer(serviceName);
|
|
25
|
+
};
|
|
26
|
+
exports.initTracer = initTracer;
|
|
27
|
+
// Global tracer instance
|
|
28
|
+
let tracer = null;
|
|
29
|
+
const getTracer = () => {
|
|
30
|
+
if (!tracer) {
|
|
31
|
+
tracer = (0, exports.initTracer)(process.env.SERVICE_NAME || 'unknown-service');
|
|
32
|
+
}
|
|
33
|
+
return tracer;
|
|
34
|
+
};
|
|
35
|
+
exports.getTracer = getTracer;
|
|
36
|
+
// Start a new span. `parent` may be a Span or a Context (e.g. from extractSpanContext).
|
|
37
|
+
const startSpan = (operationName, parentSpan) => {
|
|
38
|
+
if (!parentSpan) {
|
|
39
|
+
return (0, exports.getTracer)().startSpan(operationName);
|
|
40
|
+
}
|
|
41
|
+
const parentContext = typeof parentSpan.spanContext === 'function'
|
|
42
|
+
? api_1.trace.setSpan(api_1.context.active(), parentSpan)
|
|
43
|
+
: parentSpan;
|
|
44
|
+
return (0, exports.getTracer)().startSpan(operationName, undefined, parentContext);
|
|
45
|
+
};
|
|
46
|
+
exports.startSpan = startSpan;
|
|
47
|
+
// Middleware for Express to create spans for requests
|
|
48
|
+
const tracingMiddleware = (req, res, next) => {
|
|
49
|
+
const parentContext = api_1.propagation.extract(api_1.context.active(), req.headers);
|
|
50
|
+
const span = (0, exports.getTracer)().startSpan(`${req.method} ${req.path}`, { kind: api_1.SpanKind.SERVER }, parentContext);
|
|
51
|
+
span.setAttribute('http.method', req.method);
|
|
52
|
+
span.setAttribute('http.url', req.url);
|
|
53
|
+
// Add span to request for use in handlers
|
|
54
|
+
req.span = span;
|
|
55
|
+
res.on('finish', () => {
|
|
56
|
+
span.setAttribute('http.status_code', res.statusCode);
|
|
57
|
+
span.end();
|
|
58
|
+
});
|
|
59
|
+
next();
|
|
60
|
+
};
|
|
61
|
+
exports.tracingMiddleware = tracingMiddleware;
|
|
62
|
+
// Helper to create child spans
|
|
63
|
+
const createChildSpan = (operationName, parentSpan) => {
|
|
64
|
+
return (0, exports.startSpan)(operationName, parentSpan);
|
|
65
|
+
};
|
|
66
|
+
exports.createChildSpan = createChildSpan;
|
|
67
|
+
// Helper to log events to spans
|
|
68
|
+
const logToSpan = (span, event, data) => {
|
|
69
|
+
span.addEvent(event, data);
|
|
70
|
+
};
|
|
71
|
+
exports.logToSpan = logToSpan;
|
|
72
|
+
// Helper to set tags on spans
|
|
73
|
+
const setSpanTag = (span, key, value) => {
|
|
74
|
+
span.setAttribute(key, value);
|
|
75
|
+
};
|
|
76
|
+
exports.setSpanTag = setSpanTag;
|
|
77
|
+
// Helper to inject tracing headers (W3C traceparent) for downstream calls
|
|
78
|
+
const injectHeaders = (span) => {
|
|
79
|
+
const headers = {};
|
|
80
|
+
api_1.propagation.inject(api_1.trace.setSpan(api_1.context.active(), span), headers);
|
|
81
|
+
return headers;
|
|
82
|
+
};
|
|
83
|
+
exports.injectHeaders = injectHeaders;
|
|
84
|
+
// Helper to extract span context from incoming headers; pass the result to startSpan as parent
|
|
85
|
+
const extractSpanContext = (headers) => {
|
|
86
|
+
return api_1.propagation.extract(api_1.context.active(), headers);
|
|
87
|
+
};
|
|
88
|
+
exports.extractSpanContext = extractSpanContext;
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tracing/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface UserPayload {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
picture?: string;
|
|
6
|
+
groups?: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Attaches `user`/`userGroups` to every Express `Request` across all
|
|
10
|
+
* services, replacing the ad-hoc `(req as any).user` casts and each
|
|
11
|
+
* service's own divergent local augmentation. Individual services may still
|
|
12
|
+
* carry additional request-scoped fields (e.g. auth-service's `dbUser`) —
|
|
13
|
+
* those stay local since they aren't meaningful outside that service.
|
|
14
|
+
*/
|
|
15
|
+
declare global {
|
|
16
|
+
namespace Express {
|
|
17
|
+
interface Request {
|
|
18
|
+
user?: UserPayload;
|
|
19
|
+
userGroups?: string[];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export interface ServiceHealth {
|
|
24
|
+
status: 'ok' | 'error';
|
|
25
|
+
service: string;
|
|
26
|
+
type: 'backend' | 'frontend';
|
|
27
|
+
timestamp: string;
|
|
28
|
+
version: string;
|
|
29
|
+
description: string;
|
|
30
|
+
dependencies?: Record<string, any>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/types/auth.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,WAAW,CAAC;YACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;SACvB;KACF;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/types/auth.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RequestHandler } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a unique correlation ID for request tracing
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateCorrelationId(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Middleware to add correlation ID to requests
|
|
8
|
+
* Adds correlation ID to request object and response headers
|
|
9
|
+
*/
|
|
10
|
+
export declare const correlationMiddleware: RequestHandler;
|
|
11
|
+
//# sourceMappingURL=correlation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.d.ts","sourceRoot":"","sources":["../../src/utils/correlation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAG1E;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAOnC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.correlationMiddleware = void 0;
|
|
4
|
+
exports.generateCorrelationId = generateCorrelationId;
|
|
5
|
+
const uuid_1 = require("uuid");
|
|
6
|
+
/**
|
|
7
|
+
* Generate a unique correlation ID for request tracing
|
|
8
|
+
*/
|
|
9
|
+
function generateCorrelationId() {
|
|
10
|
+
return (0, uuid_1.v4)();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Middleware to add correlation ID to requests
|
|
14
|
+
* Adds correlation ID to request object and response headers
|
|
15
|
+
*/
|
|
16
|
+
const correlationMiddleware = (req, res, next) => {
|
|
17
|
+
const correlationId = req.headers['x-correlation-id'] || generateCorrelationId();
|
|
18
|
+
req.correlationId = correlationId;
|
|
19
|
+
res.setHeader('x-correlation-id', correlationId);
|
|
20
|
+
next();
|
|
21
|
+
};
|
|
22
|
+
exports.correlationMiddleware = correlationMiddleware;
|
|
23
|
+
//# sourceMappingURL=correlation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.js","sourceRoot":"","sources":["../../src/utils/correlation.ts"],"names":[],"mappings":";;;AAMA,sDAEC;AAPD,+BAAoC;AAEpC;;GAEG;AACH,SAAgB,qBAAqB;IACnC,OAAO,IAAA,SAAM,GAAE,CAAC;AAClB,CAAC;AAED;;;GAGG;AACI,MAAM,qBAAqB,GAAmB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACvG,MAAM,aAAa,GAAG,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAW,IAAI,qBAAqB,EAAE,CAAC;IAE1F,GAAW,CAAC,aAAa,GAAG,aAAa,CAAC;IAC3C,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;IAEjD,IAAI,EAAE,CAAC;AACT,CAAC,CAAC;AAPW,QAAA,qBAAqB,yBAOhC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Standard API response format
|
|
4
|
+
*/
|
|
5
|
+
export interface ApiResponse<T = any> {
|
|
6
|
+
success: boolean;
|
|
7
|
+
data?: T;
|
|
8
|
+
message?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
correlationId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a standardized API response
|
|
14
|
+
*/
|
|
15
|
+
export declare function createApiResponse<T = any>(success: boolean, data?: T, message?: string, error?: string, correlationId?: string): ApiResponse<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Send a success response
|
|
18
|
+
*/
|
|
19
|
+
export declare function sendSuccess<T = any>(res: Response, data?: T, message?: string, statusCode?: number, correlationId?: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Send an error response
|
|
22
|
+
*/
|
|
23
|
+
export declare function sendError(res: Response, message: string, statusCode?: number, error?: string, correlationId?: string): void;
|
|
24
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/utils/response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,GAAG;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,GAAG,GAAG,EACvC,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,CAAC,EACR,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GACrB,WAAW,CAAC,CAAC,CAAC,CAShB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,GAAG,EACjC,GAAG,EAAE,QAAQ,EACb,IAAI,CAAC,EAAE,CAAC,EACR,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,GAAE,MAAY,EACxB,aAAa,CAAC,EAAE,MAAM,QAIvB;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,MAAY,EACxB,KAAK,CAAC,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,QAIvB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiResponse = createApiResponse;
|
|
4
|
+
exports.sendSuccess = sendSuccess;
|
|
5
|
+
exports.sendError = sendError;
|
|
6
|
+
/**
|
|
7
|
+
* Create a standardized API response
|
|
8
|
+
*/
|
|
9
|
+
function createApiResponse(success, data, message, error, correlationId) {
|
|
10
|
+
const response = { success };
|
|
11
|
+
if (data !== undefined)
|
|
12
|
+
response.data = data;
|
|
13
|
+
if (message)
|
|
14
|
+
response.message = message;
|
|
15
|
+
if (error)
|
|
16
|
+
response.error = error;
|
|
17
|
+
if (correlationId)
|
|
18
|
+
response.correlationId = correlationId;
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Send a success response
|
|
23
|
+
*/
|
|
24
|
+
function sendSuccess(res, data, message, statusCode = 200, correlationId) {
|
|
25
|
+
const response = createApiResponse(true, data, message, undefined, correlationId);
|
|
26
|
+
res.status(statusCode).json(response);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Send an error response
|
|
30
|
+
*/
|
|
31
|
+
function sendError(res, message, statusCode = 500, error, correlationId) {
|
|
32
|
+
const response = createApiResponse(false, undefined, message, error, correlationId);
|
|
33
|
+
res.status(statusCode).json(response);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/utils/response.ts"],"names":[],"mappings":";;AAgBA,8CAeC;AAKD,kCASC;AAKD,8BASC;AA9CD;;GAEG;AACH,SAAgB,iBAAiB,CAC/B,OAAgB,EAChB,IAAQ,EACR,OAAgB,EAChB,KAAc,EACd,aAAsB;IAEtB,MAAM,QAAQ,GAAmB,EAAE,OAAO,EAAE,CAAC;IAE7C,IAAI,IAAI,KAAK,SAAS;QAAE,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;IAC7C,IAAI,OAAO;QAAE,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC;IACxC,IAAI,KAAK;QAAE,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;IAClC,IAAI,aAAa;QAAE,QAAQ,CAAC,aAAa,GAAG,aAAa,CAAC;IAE1D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,SAAgB,WAAW,CACzB,GAAa,EACb,IAAQ,EACR,OAAgB,EAChB,aAAqB,GAAG,EACxB,aAAsB;IAEtB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;IAClF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC;AAED;;GAEG;AACH,SAAgB,SAAS,CACvB,GAAa,EACb,OAAe,EACf,aAAqB,GAAG,EACxB,KAAc,EACd,aAAsB;IAEtB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;IACpF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC"}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
testMatch: ['<rootDir>/src/**/*.test.ts'],
|
|
6
|
+
clearMocks: true,
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.ts$': ['ts-jest', { tsconfig: { types: ['jest', 'node'] } }]
|
|
9
|
+
},
|
|
10
|
+
coverageProvider: 'v8',
|
|
11
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts', '!src/index.ts', '!src/types/**'],
|
|
12
|
+
coverageReporters: ['text', 'json-summary'],
|
|
13
|
+
coverageThreshold: { global: { statements: 90, branches: 80, functions: 90, lines: 90 } }
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tumbaland/backend-core",
|
|
3
|
+
"version": "1.16.0",
|
|
4
|
+
"description": "Core shared functionality for Tumbaland backend services",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.build.json",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"clean": "rm -rf dist",
|
|
11
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
12
|
+
"release": "standard-version && npm run build && npm publish --access public",
|
|
13
|
+
"release:beta": "standard-version --prerelease beta && npm run build && npm publish --access public --tag beta",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:coverage": "jest --coverage",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"tumbaland",
|
|
21
|
+
"backend",
|
|
22
|
+
"shared",
|
|
23
|
+
"core",
|
|
24
|
+
"microservices"
|
|
25
|
+
],
|
|
26
|
+
"author": "Tumbaland Team",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/cookie-parser": "^1.4.10",
|
|
30
|
+
"@types/cors": "^2.8.19",
|
|
31
|
+
"@types/express": "^5.0.6",
|
|
32
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
33
|
+
"@types/morgan": "^1.9.10",
|
|
34
|
+
"@types/node": "^26.1.0",
|
|
35
|
+
"standard-version": "^9.5.0",
|
|
36
|
+
"typescript": "^6.0.3"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@opentelemetry/api": "^1.9.1",
|
|
40
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
|
|
41
|
+
"@opentelemetry/resources": "^2.9.0",
|
|
42
|
+
"@opentelemetry/sdk-trace-node": "^2.9.0",
|
|
43
|
+
"@opentelemetry/semantic-conventions": "^1.41.1",
|
|
44
|
+
"cookie-parser": "^1.4.7",
|
|
45
|
+
"cors": "^2.8.6",
|
|
46
|
+
"express": "^5.1.0",
|
|
47
|
+
"express-rate-limit": "^8.5.2",
|
|
48
|
+
"helmet": "^8.0.0",
|
|
49
|
+
"jsonwebtoken": "^9.0.3",
|
|
50
|
+
"mongoose": "^9.7.3",
|
|
51
|
+
"morgan": "^1.11.0",
|
|
52
|
+
"prom-client": "^15.1.3",
|
|
53
|
+
"uuid": "^14.0.1",
|
|
54
|
+
"winston": "^3.19.0",
|
|
55
|
+
"zod": "^4.4.3"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"express": "^5.1.0",
|
|
59
|
+
"mongoose": "^9.7.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { createBaseApp } from './createBaseApp';
|
|
3
|
+
|
|
4
|
+
jest.mock('../logging/logger', () => ({
|
|
5
|
+
__esModule: true,
|
|
6
|
+
default: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn(), http: jest.fn() }
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('createBaseApp', () => {
|
|
10
|
+
it('parses JSON bodies and cookies by default', async () => {
|
|
11
|
+
const app = createBaseApp();
|
|
12
|
+
app.get('/echo-cookie', (req, res) => res.json({ cookies: req.cookies }));
|
|
13
|
+
app.post('/echo-body', (req, res) => res.json({ body: req.body }));
|
|
14
|
+
|
|
15
|
+
const cookieRes = await request(app).get('/echo-cookie').set('Cookie', 'foo=bar');
|
|
16
|
+
expect(cookieRes.body).toEqual({ cookies: { foo: 'bar' } });
|
|
17
|
+
|
|
18
|
+
const bodyRes = await request(app).post('/echo-body').send({ hello: 'world' });
|
|
19
|
+
expect(bodyRes.body).toEqual({ body: { hello: 'world' } });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('applies security headers and rate limiting', async () => {
|
|
23
|
+
const app = createBaseApp();
|
|
24
|
+
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
|
25
|
+
|
|
26
|
+
const res = await request(app).get('/ping');
|
|
27
|
+
expect(res.headers['x-content-type-options']).toBe('nosniff');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('skips JSON parsing when parseJson is false', async () => {
|
|
31
|
+
const app = createBaseApp({ parseJson: false });
|
|
32
|
+
app.post('/echo-body', (req, res) => res.json({ body: req.body ?? null }));
|
|
33
|
+
|
|
34
|
+
const res = await request(app).post('/echo-body').set('Content-Type', 'application/json').send('{"hello":"world"}');
|
|
35
|
+
expect(res.body).toEqual({ body: null });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('skips cookie parsing when parseCookies is false', async () => {
|
|
39
|
+
const app = createBaseApp({ parseCookies: false });
|
|
40
|
+
app.get('/echo-cookie', (req, res) => res.json({ cookies: req.cookies ?? null }));
|
|
41
|
+
|
|
42
|
+
const res = await request(app).get('/echo-cookie').set('Cookie', 'foo=bar');
|
|
43
|
+
expect(res.body).toEqual({ cookies: null });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('honors a custom rate limiter', async () => {
|
|
47
|
+
const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
|
|
48
|
+
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
|
49
|
+
|
|
50
|
+
const first = await request(app).get('/ping');
|
|
51
|
+
const second = await request(app).get('/ping');
|
|
52
|
+
|
|
53
|
+
expect(first.status).toBe(200);
|
|
54
|
+
expect(second.status).toBe(429);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('mounts /health/live ahead of the rate limiter, so it never gets 429s a busy service would', async () => {
|
|
58
|
+
const app = createBaseApp({ rateLimiter: (await import('../middleware/security')).createRateLimiter({ windowMs: 60_000, max: 1 }) });
|
|
59
|
+
app.get('/ping', (_req, res) => res.json({ ok: true }));
|
|
60
|
+
|
|
61
|
+
await request(app).get('/ping'); // exhaust the max: 1 budget
|
|
62
|
+
const live1 = await request(app).get('/health/live');
|
|
63
|
+
const live2 = await request(app).get('/health/live');
|
|
64
|
+
|
|
65
|
+
expect(live1.status).toBe(200);
|
|
66
|
+
expect(live1.body).toEqual({ status: 'ok' });
|
|
67
|
+
expect(live2.status).toBe(200);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import express, { Express, RequestHandler } from 'express';
|
|
2
|
+
import cookieParser from 'cookie-parser';
|
|
3
|
+
import { correlationMiddleware } from '../utils/correlation';
|
|
4
|
+
import { tracingMiddleware } from '../tracing';
|
|
5
|
+
import { requestLogger, requestLoggerWithMetrics } from '../middleware/requestLogger';
|
|
6
|
+
import { createCorsMiddleware, CorsMiddlewareOptions } from '../middleware/corsMiddleware';
|
|
7
|
+
import { securityHeaders, standardRateLimiter } from '../middleware/security';
|
|
8
|
+
import { healthLive } from '../health/healthController';
|
|
9
|
+
|
|
10
|
+
export interface CreateBaseAppOptions {
|
|
11
|
+
/** Passed through to createCorsMiddleware. */
|
|
12
|
+
corsOptions?: CorsMiddlewareOptions;
|
|
13
|
+
/**
|
|
14
|
+
* Defaults to standardRateLimiter; pass strictRateLimiter or a custom one
|
|
15
|
+
* to override. Pass `false` to skip mounting one here entirely — e.g. when
|
|
16
|
+
* a route (like a webhook) must be registered, and therefore exempted,
|
|
17
|
+
* before the limiter, and the service applies it itself afterward.
|
|
18
|
+
*/
|
|
19
|
+
rateLimiter?: RequestHandler | false;
|
|
20
|
+
/** Mount express.json() here. Set false when a route needs the raw body first (e.g. a webhook) or handles its own parsing. Default true. */
|
|
21
|
+
parseJson?: boolean;
|
|
22
|
+
/** Mount cookie-parser here. Set false for services that don't use cookies. Default true. */
|
|
23
|
+
parseCookies?: boolean;
|
|
24
|
+
/** Mount tracingMiddleware. Default true. */
|
|
25
|
+
tracing?: boolean;
|
|
26
|
+
/** Use requestLoggerWithMetrics (adds Prometheus histograms) instead of the plain requestLogger. Default true. */
|
|
27
|
+
metrics?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Assembles the middleware stack every service was hand-rolling in its own
|
|
32
|
+
* index.ts (correlation → tracing → request logging → security → rate
|
|
33
|
+
* limiting → CORS → cookies → JSON body). Services still mount their own
|
|
34
|
+
* routes, `/health` (DB-aware readiness), `/metrics`, and
|
|
35
|
+
* `app.use(errorHandler)` last — this only owns the common prefix, not the
|
|
36
|
+
* whole app lifecycle.
|
|
37
|
+
*
|
|
38
|
+
* `/health/live` is the one exception: it's mounted here, first, ahead of
|
|
39
|
+
* every other middleware. It has zero per-service variation (it never
|
|
40
|
+
* touches a dependency, just confirms the process is up), so — unlike
|
|
41
|
+
* `/health` — there's nothing for a service to customize. Mounting it
|
|
42
|
+
* before the rate limiter also matters operationally: a service under
|
|
43
|
+
* heavy legitimate traffic shouldn't have its own liveness probe start
|
|
44
|
+
* getting 429s and get killed for being "unhealthy" at the exact moment
|
|
45
|
+
* it's just busy.
|
|
46
|
+
*
|
|
47
|
+
* Services with non-standard body parsing (a webhook needing the raw body,
|
|
48
|
+
* or conditional parsing per-route) should pass `parseJson`/`parseCookies:
|
|
49
|
+
* false` and mount those themselves at the exact point they need to.
|
|
50
|
+
*/
|
|
51
|
+
export function createBaseApp(options: CreateBaseAppOptions = {}): Express {
|
|
52
|
+
const {
|
|
53
|
+
corsOptions,
|
|
54
|
+
rateLimiter = standardRateLimiter,
|
|
55
|
+
parseJson = true,
|
|
56
|
+
parseCookies = true,
|
|
57
|
+
tracing = true,
|
|
58
|
+
metrics = true
|
|
59
|
+
} = options;
|
|
60
|
+
|
|
61
|
+
const app = express();
|
|
62
|
+
|
|
63
|
+
app.get('/health/live', healthLive);
|
|
64
|
+
|
|
65
|
+
app.use(securityHeaders);
|
|
66
|
+
if (rateLimiter) app.use(rateLimiter);
|
|
67
|
+
app.use(correlationMiddleware);
|
|
68
|
+
if (tracing) app.use(tracingMiddleware);
|
|
69
|
+
app.use(metrics ? requestLoggerWithMetrics : requestLogger);
|
|
70
|
+
app.use(createCorsMiddleware(corsOptions));
|
|
71
|
+
if (parseCookies) app.use(cookieParser());
|
|
72
|
+
if (parseJson) app.use(express.json());
|
|
73
|
+
|
|
74
|
+
return app;
|
|
75
|
+
}
|