@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.
Files changed (113) hide show
  1. package/.versionrc.json +7 -0
  2. package/README.md +179 -0
  3. package/__mocks__/uuid.js +8 -0
  4. package/dist/app/createBaseApp.d.ts +44 -0
  5. package/dist/app/createBaseApp.d.ts.map +1 -0
  6. package/dist/app/createBaseApp.js +54 -0
  7. package/dist/app/createBaseApp.js.map +1 -0
  8. package/dist/config/env.d.ts +8 -0
  9. package/dist/config/env.d.ts.map +1 -0
  10. package/dist/config/env.js +17 -0
  11. package/dist/config/env.js.map +1 -0
  12. package/dist/database/connection.d.ts +3 -0
  13. package/dist/database/connection.d.ts.map +1 -0
  14. package/dist/database/connection.js +49 -0
  15. package/dist/database/connection.js.map +1 -0
  16. package/dist/errors/HttpError.d.ts +32 -0
  17. package/dist/errors/HttpError.d.ts.map +1 -0
  18. package/dist/errors/HttpError.js +61 -0
  19. package/dist/errors/HttpError.js.map +1 -0
  20. package/dist/health/healthController.d.ts +21 -0
  21. package/dist/health/healthController.d.ts.map +1 -0
  22. package/dist/health/healthController.js +56 -0
  23. package/dist/health/healthController.js.map +1 -0
  24. package/dist/index.d.ts +20 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +81 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/logging/logger.d.ts +4 -0
  29. package/dist/logging/logger.d.ts.map +1 -0
  30. package/dist/logging/logger.js +78 -0
  31. package/dist/logging/logger.js.map +1 -0
  32. package/dist/metrics/index.d.ts +17 -0
  33. package/dist/metrics/index.d.ts.map +1 -0
  34. package/dist/metrics/index.js +99 -0
  35. package/dist/metrics/index.js.map +1 -0
  36. package/dist/middleware/authMiddleware.d.ts +9 -0
  37. package/dist/middleware/authMiddleware.d.ts.map +1 -0
  38. package/dist/middleware/authMiddleware.js +32 -0
  39. package/dist/middleware/authMiddleware.js.map +1 -0
  40. package/dist/middleware/corsMiddleware.d.ts +20 -0
  41. package/dist/middleware/corsMiddleware.d.ts.map +1 -0
  42. package/dist/middleware/corsMiddleware.js +33 -0
  43. package/dist/middleware/corsMiddleware.js.map +1 -0
  44. package/dist/middleware/errorHandler.d.ts +14 -0
  45. package/dist/middleware/errorHandler.d.ts.map +1 -0
  46. package/dist/middleware/errorHandler.js +88 -0
  47. package/dist/middleware/errorHandler.js.map +1 -0
  48. package/dist/middleware/requestLogger.d.ts +16 -0
  49. package/dist/middleware/requestLogger.d.ts.map +1 -0
  50. package/dist/middleware/requestLogger.js +49 -0
  51. package/dist/middleware/requestLogger.js.map +1 -0
  52. package/dist/middleware/security.d.ts +26 -0
  53. package/dist/middleware/security.d.ts.map +1 -0
  54. package/dist/middleware/security.js +47 -0
  55. package/dist/middleware/security.js.map +1 -0
  56. package/dist/middleware/validate.d.ts +11 -0
  57. package/dist/middleware/validate.d.ts.map +1 -0
  58. package/dist/middleware/validate.js +24 -0
  59. package/dist/middleware/validate.js.map +1 -0
  60. package/dist/tracing/index.d.ts +13 -0
  61. package/dist/tracing/index.d.ts.map +1 -0
  62. package/dist/tracing/index.js +89 -0
  63. package/dist/tracing/index.js.map +1 -0
  64. package/dist/types/auth.d.ts +32 -0
  65. package/dist/types/auth.d.ts.map +1 -0
  66. package/dist/types/auth.js +3 -0
  67. package/dist/types/auth.js.map +1 -0
  68. package/dist/utils/correlation.d.ts +11 -0
  69. package/dist/utils/correlation.d.ts.map +1 -0
  70. package/dist/utils/correlation.js +23 -0
  71. package/dist/utils/correlation.js.map +1 -0
  72. package/dist/utils/response.d.ts +24 -0
  73. package/dist/utils/response.d.ts.map +1 -0
  74. package/dist/utils/response.js +35 -0
  75. package/dist/utils/response.js.map +1 -0
  76. package/jest.config.js +14 -0
  77. package/package.json +61 -0
  78. package/src/app/createBaseApp.test.ts +69 -0
  79. package/src/app/createBaseApp.ts +75 -0
  80. package/src/config/env.test.ts +28 -0
  81. package/src/config/env.ts +13 -0
  82. package/src/database/connection.test.ts +142 -0
  83. package/src/database/connection.ts +46 -0
  84. package/src/errors/HttpError.test.ts +38 -0
  85. package/src/errors/HttpError.ts +57 -0
  86. package/src/health/healthController.test.ts +91 -0
  87. package/src/health/healthController.ts +56 -0
  88. package/src/index.ts +60 -0
  89. package/src/logging/logger.test.ts +76 -0
  90. package/src/logging/logger.ts +103 -0
  91. package/src/metrics/index.test.ts +91 -0
  92. package/src/metrics/index.ts +110 -0
  93. package/src/middleware/authMiddleware.test.ts +104 -0
  94. package/src/middleware/authMiddleware.ts +29 -0
  95. package/src/middleware/corsMiddleware.test.ts +58 -0
  96. package/src/middleware/corsMiddleware.ts +36 -0
  97. package/src/middleware/errorHandler.test.ts +146 -0
  98. package/src/middleware/errorHandler.ts +98 -0
  99. package/src/middleware/requestLogger.test.ts +81 -0
  100. package/src/middleware/requestLogger.ts +47 -0
  101. package/src/middleware/security.test.ts +45 -0
  102. package/src/middleware/security.ts +43 -0
  103. package/src/middleware/validate.test.ts +60 -0
  104. package/src/middleware/validate.ts +23 -0
  105. package/src/tracing/index.test.ts +250 -0
  106. package/src/tracing/index.ts +97 -0
  107. package/src/types/auth.ts +33 -0
  108. package/src/utils/correlation.test.ts +47 -0
  109. package/src/utils/correlation.ts +22 -0
  110. package/src/utils/response.test.ts +64 -0
  111. package/src/utils/response.ts +60 -0
  112. package/tsconfig.build.json +4 -0
  113. 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,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=auth.js.map
@@ -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
+ }