@takaro/http 0.0.0-next.0da151e

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 (59) hide show
  1. package/README.md +7 -0
  2. package/dist/app.d.ts +21 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +86 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/controllers/meta.d.ts +18 -0
  7. package/dist/controllers/meta.d.ts.map +1 -0
  8. package/dist/controllers/meta.js +284 -0
  9. package/dist/controllers/meta.js.map +1 -0
  10. package/dist/main.d.ts +8 -0
  11. package/dist/main.d.ts.map +1 -0
  12. package/dist/main.js +8 -0
  13. package/dist/main.js.map +1 -0
  14. package/dist/middleware/basicAuth.d.ts +3 -0
  15. package/dist/middleware/basicAuth.d.ts.map +1 -0
  16. package/dist/middleware/basicAuth.js +27 -0
  17. package/dist/middleware/basicAuth.js.map +1 -0
  18. package/dist/middleware/errorHandler.d.ts +3 -0
  19. package/dist/middleware/errorHandler.d.ts.map +1 -0
  20. package/dist/middleware/errorHandler.js +73 -0
  21. package/dist/middleware/errorHandler.js.map +1 -0
  22. package/dist/middleware/logger.d.ts +9 -0
  23. package/dist/middleware/logger.d.ts.map +1 -0
  24. package/dist/middleware/logger.js +51 -0
  25. package/dist/middleware/logger.js.map +1 -0
  26. package/dist/middleware/metrics.d.ts +3 -0
  27. package/dist/middleware/metrics.d.ts.map +1 -0
  28. package/dist/middleware/metrics.js +35 -0
  29. package/dist/middleware/metrics.js.map +1 -0
  30. package/dist/middleware/paginationMiddleware.d.ts +3 -0
  31. package/dist/middleware/paginationMiddleware.d.ts.map +1 -0
  32. package/dist/middleware/paginationMiddleware.js +26 -0
  33. package/dist/middleware/paginationMiddleware.js.map +1 -0
  34. package/dist/middleware/rateLimit.d.ts +9 -0
  35. package/dist/middleware/rateLimit.d.ts.map +1 -0
  36. package/dist/middleware/rateLimit.js +60 -0
  37. package/dist/middleware/rateLimit.js.map +1 -0
  38. package/dist/util/apiResponse.d.ts +37 -0
  39. package/dist/util/apiResponse.d.ts.map +1 -0
  40. package/dist/util/apiResponse.js +99 -0
  41. package/dist/util/apiResponse.js.map +1 -0
  42. package/package.json +15 -0
  43. package/src/app.ts +105 -0
  44. package/src/controllers/__tests__/meta.integration.test.ts +23 -0
  45. package/src/controllers/defaultMetadatastorage.d.ts +5 -0
  46. package/src/controllers/meta.ts +268 -0
  47. package/src/main.ts +8 -0
  48. package/src/middleware/__tests__/paginationMiddleware.unit.test.ts +49 -0
  49. package/src/middleware/__tests__/rateLimit.integration.test.ts +130 -0
  50. package/src/middleware/basicAuth.ts +34 -0
  51. package/src/middleware/errorHandler.ts +95 -0
  52. package/src/middleware/logger.ts +62 -0
  53. package/src/middleware/metrics.ts +42 -0
  54. package/src/middleware/paginationMiddleware.ts +29 -0
  55. package/src/middleware/rateLimit.ts +73 -0
  56. package/src/util/apiResponse.ts +112 -0
  57. package/tsconfig.build.json +9 -0
  58. package/tsconfig.json +8 -0
  59. package/typedoc.json +3 -0
@@ -0,0 +1,51 @@
1
+ import { logger, ctx } from '@takaro/util';
2
+ import { context, trace } from '@opentelemetry/api';
3
+ const SUPPRESS_BODY_KEYWORDS = ['password', 'newPassword'];
4
+ const HIDDEN_ROUTES = ['/metrics', '/health', '/healthz', '/ready', '/readyz', '/queues/api/queues'];
5
+ const log = logger('http');
6
+ /**
7
+ * This middleware is called very early in the request lifecycle, so it's
8
+ * we leverage this fact to inject the context tracking at this stage
9
+ */
10
+ export const LoggingMiddleware = ctx.wrap('HTTP', loggingMiddleware);
11
+ async function loggingMiddleware(req, res, next) {
12
+ if (HIDDEN_ROUTES.some((route) => req.originalUrl.startsWith(route))) {
13
+ return next();
14
+ }
15
+ const requestStartMs = Date.now();
16
+ const hideData = SUPPRESS_BODY_KEYWORDS.some((keyword) => (JSON.stringify(req.body) || '').includes(keyword));
17
+ log.debug(`⬇️ ${req.method} ${req.originalUrl}`, {
18
+ ip: req.ip,
19
+ method: req.method,
20
+ path: req.originalUrl,
21
+ body: hideData ? { suppressed_output: true } : req.body,
22
+ });
23
+ const span = trace.getSpan(context.active());
24
+ if (span) {
25
+ // get the trace ID from the span and set it in the headers
26
+ const traceId = span.spanContext().traceId;
27
+ res.header('X-Trace-Id', traceId);
28
+ }
29
+ // Log on API Call Finish to add responseTime
30
+ res.once('finish', () => {
31
+ const responseTime = Date.now() - requestStartMs;
32
+ log.info(`⬆️ ${req.method} ${req.originalUrl}`, {
33
+ responseTime,
34
+ requestMethod: req.method,
35
+ requestUrl: req.originalUrl,
36
+ requestSize: req.headers['content-length'],
37
+ status: res.statusCode,
38
+ responseSize: res.getHeader('Content-Length'),
39
+ userAgent: req.get('User-Agent'),
40
+ remoteIp: req.ip,
41
+ serverIp: '127.0.0.1',
42
+ referer: req.get('Referer'),
43
+ cacheLookup: false,
44
+ cacheHit: false,
45
+ cacheValidatedWithOriginServer: false,
46
+ protocol: req.protocol,
47
+ });
48
+ });
49
+ next();
50
+ }
51
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/middleware/logger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,sBAAsB,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAC3D,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;AACrG,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAE3B;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAA6B,CAAC;AAEjG,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC9E,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACrE,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAElC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAE9G,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,EAAE;QAC/C,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,WAAW;QACrB,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;KACxD,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE7C,IAAI,IAAI,EAAE,CAAC;QACT,2DAA2D;QAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,6CAA6C;IAC7C,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE;QACtB,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC;QAEjD,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,EAAE;YAC9C,YAAY;YACZ,aAAa,EAAE,GAAG,CAAC,MAAM;YACzB,UAAU,EAAE,GAAG,CAAC,WAAW;YAC3B,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC;YAC1C,MAAM,EAAE,GAAG,CAAC,UAAU;YACtB,YAAY,EAAE,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC;YAC7C,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;YAChC,QAAQ,EAAE,GAAG,CAAC,EAAE;YAChB,QAAQ,EAAE,WAAW;YACrB,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;YAC3B,WAAW,EAAE,KAAK;YAClB,QAAQ,EAAE,KAAK;YACf,8BAA8B,EAAE,KAAK;YACrC,QAAQ,EAAE,GAAG,CAAC,QAAQ;SACvB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export declare function metricsMiddleware(req: Request, res: Response, next: NextFunction): Promise<void>;
3
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/middleware/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAgB1D,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,iBAyBtF"}
@@ -0,0 +1,35 @@
1
+ import { Counter, Histogram } from 'prom-client';
2
+ const counter = new Counter({
3
+ name: 'http_requests_total',
4
+ help: 'Total number of HTTP requests made',
5
+ labelNames: ['path', 'method', 'status'],
6
+ });
7
+ const histogram = new Histogram({
8
+ name: 'http_request_duration_seconds',
9
+ help: 'Duration of HTTP requests in seconds',
10
+ labelNames: ['path', 'method', 'status'],
11
+ buckets: [0.1, 0.5, 1, 2, 5, 10],
12
+ });
13
+ export async function metricsMiddleware(req, res, next) {
14
+ const rawPath = req.path;
15
+ const method = req.method;
16
+ // Filter out anything that looks like a UUID from path
17
+ const path = rawPath.replace(/\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, '/:id');
18
+ const start = Date.now();
19
+ try {
20
+ next();
21
+ }
22
+ finally {
23
+ counter.inc({
24
+ path,
25
+ method,
26
+ status: res.statusCode.toString(),
27
+ });
28
+ histogram.observe({
29
+ path,
30
+ method,
31
+ status: res.statusCode.toString(),
32
+ }, (Date.now() - start) / 1000);
33
+ }
34
+ }
35
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/middleware/metrics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;IAC1B,IAAI,EAAE,qBAAqB;IAC3B,IAAI,EAAE,oCAAoC;IAC1C,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;CACzC,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;IAC9B,IAAI,EAAE,+BAA+B;IACrC,IAAI,EAAE,sCAAsC;IAC5C,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACxC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACrF,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAC1B,uDAAuD;IACvD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,iEAAiE,EAAE,MAAM,CAAC,CAAC;IAExG,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEzB,IAAI,CAAC;QACH,IAAI,EAAE,CAAC;IACT,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,GAAG,CAAC;YACV,IAAI;YACJ,MAAM;YACN,MAAM,EAAE,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE;SAClC,CAAC,CAAC;QACH,SAAS,CAAC,OAAO,CACf;YACE,IAAI;YACJ,MAAM;YACN,MAAM,EAAE,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE;SAClC,EACD,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,IAAI,CAC5B,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { NextFunction, Request, Response } from 'express';
2
+ export declare function paginationMiddleware(req: Request, res: Response, next: NextFunction): Promise<void>;
3
+ //# sourceMappingURL=paginationMiddleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginationMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/paginationMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAW1D,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzG"}
@@ -0,0 +1,26 @@
1
+ import * as yup from 'yup';
2
+ import { errors, logger } from '@takaro/util';
3
+ const log = logger('http:pagination');
4
+ const paginationSchema = yup.object({
5
+ page: yup.number().default(0).min(0),
6
+ limit: yup.number().default(100).min(1).max(1000),
7
+ });
8
+ export async function paginationMiddleware(req, res, next) {
9
+ const merged = { ...req.query, ...req.body };
10
+ try {
11
+ const result = await paginationSchema.validate(merged);
12
+ res.locals.page = result.page;
13
+ res.locals.limit = result.limit;
14
+ next();
15
+ }
16
+ catch (error) {
17
+ if (error instanceof yup.ValidationError) {
18
+ next(new errors.ValidationError('Invalid pagination', [error]));
19
+ }
20
+ else {
21
+ log.error('Unexpected error', error);
22
+ next(new errors.InternalServerError());
23
+ }
24
+ }
25
+ }
26
+ //# sourceMappingURL=paginationMiddleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginationMiddleware.js","sourceRoot":"","sources":["../../src/middleware/paginationMiddleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAEtC,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;CAClD,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACxF,MAAM,MAAM,GAAG,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAEvD,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QAC9B,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAEhC,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,GAAG,CAAC,eAAe,EAAE,CAAC;YACzC,IAAI,CAAC,IAAI,MAAM,CAAC,eAAe,CAAC,oBAAoB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export interface IRateLimitMiddlewareOptions {
3
+ max: number;
4
+ windowSeconds: number;
5
+ keyPrefix?: string;
6
+ useInMemory?: boolean;
7
+ }
8
+ export declare function createRateLimitMiddleware(opts: IRateLimitMiddlewareOptions): Promise<(req: Request, res: Response, next: NextFunction) => Promise<void>>;
9
+ //# sourceMappingURL=rateLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,MAAM,WAAW,2BAA2B;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,2BAA2B,iBAyB5D,OAAO,OAAO,QAAQ,QAAQ,YAAY,oBAmC9D"}
@@ -0,0 +1,60 @@
1
+ import { Redis } from '@takaro/db';
2
+ import { logger, errors, ctx } from '@takaro/util';
3
+ import { RateLimiterRes, RateLimiterRedis, RateLimiterMemory } from 'rate-limiter-flexible';
4
+ export async function createRateLimitMiddleware(opts) {
5
+ const log = logger('http:rateLimit');
6
+ const redis = await Redis.getClient('http:rateLimit');
7
+ // We create a randomHash to use in Redis keys
8
+ // This makes sure that each endpoint can get different rate limits without too much hassle
9
+ const randomHash = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
10
+ let rateLimiter;
11
+ if (opts.useInMemory) {
12
+ rateLimiter = new RateLimiterMemory({
13
+ points: opts.max,
14
+ duration: opts.windowSeconds,
15
+ keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
16
+ });
17
+ }
18
+ else {
19
+ rateLimiter = new RateLimiterRedis({
20
+ points: opts.max,
21
+ duration: opts.windowSeconds,
22
+ storeClient: redis,
23
+ keyPrefix: `http:rateLimit:${opts.keyPrefix ? opts.keyPrefix : randomHash}`,
24
+ });
25
+ }
26
+ return async (req, res, next) => {
27
+ const ctxData = ctx.data;
28
+ let limitedKey = null;
29
+ if (ctxData.user) {
30
+ limitedKey = ctxData.user;
31
+ }
32
+ else {
33
+ // TODO: should handle case ip is undefined
34
+ limitedKey = req.ip;
35
+ }
36
+ let rateLimiterRes = null;
37
+ try {
38
+ rateLimiterRes = await rateLimiter.consume(limitedKey);
39
+ }
40
+ catch (err) {
41
+ if (err instanceof RateLimiterRes) {
42
+ rateLimiterRes = err;
43
+ log.warn(`rate limited, try again in ${err.msBeforeNext}ms`);
44
+ }
45
+ else {
46
+ throw err;
47
+ }
48
+ }
49
+ if (rateLimiterRes) {
50
+ res.set('X-RateLimit-Limit', opts.max.toString());
51
+ res.set('X-RateLimit-Remaining', rateLimiterRes.remainingPoints.toString());
52
+ res.set('X-RateLimit-Reset', rateLimiterRes.msBeforeNext.toString());
53
+ if (rateLimiterRes.remainingPoints === 0) {
54
+ next(new errors.TooManyRequestsError());
55
+ }
56
+ }
57
+ return next();
58
+ };
59
+ }
60
+ //# sourceMappingURL=rateLimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { TakaroDTO } from '@takaro/util';
2
+ import { ValidationError as ClassValidatorError } from 'class-validator';
3
+ import { Request, Response } from 'express';
4
+ declare class ErrorOutput {
5
+ code?: string;
6
+ message?: string;
7
+ details?: string | ClassValidatorError[];
8
+ }
9
+ declare class MetadataOutput {
10
+ serverTime: string;
11
+ error?: ErrorOutput;
12
+ /**
13
+ * The page number of the response
14
+ */
15
+ page?: number;
16
+ /**
17
+ * The number of items returned in the response (aka page size)
18
+ */
19
+ limit?: number;
20
+ /**
21
+ * The total number of items in the collection
22
+ */
23
+ total?: number;
24
+ }
25
+ export declare class APIOutput<T> extends TakaroDTO<APIOutput<T>> {
26
+ meta: MetadataOutput;
27
+ data: T;
28
+ }
29
+ interface IApiResponseOptions {
30
+ error?: Error;
31
+ meta?: Record<string, string | number>;
32
+ req: Request;
33
+ res: Response;
34
+ }
35
+ export declare function apiResponse(data?: unknown, opts?: IApiResponseOptions): APIOutput<unknown>;
36
+ export {};
37
+ //# sourceMappingURL=apiResponse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiResponse.d.ts","sourceRoot":"","sources":["../../src/util/apiResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9D,OAAO,EAML,eAAe,IAAI,mBAAmB,EACvC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE5C,cAAM,WAAW;IAEf,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,OAAO,CAAC,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAAC;CAC1C;AAED,cAAM,cAAc;IAGlB,UAAU,EAAG,MAAM,CAAC;IAIpB,KAAK,CAAC,EAAE,WAAW,CAAC;IAEpB;;OAEG;IAGH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IAGH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IAGH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AACD,qBAAa,SAAS,CAAC,CAAC,CAAE,SAAQ,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAGvD,IAAI,EAAG,cAAc,CAAC;IAEtB,IAAI,EAAG,CAAC,CAAC;CACV;AAED,UAAU,mBAAmB;IAC3B,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,QAAQ,CAAC;CACf;AAED,wBAAgB,WAAW,CAAC,IAAI,GAAE,OAAY,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC,OAAO,CAAC,CA2C9F"}
@@ -0,0 +1,99 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { errors, isTakaroDTO, TakaroDTO } from '@takaro/util';
11
+ import { Type } from 'class-transformer';
12
+ import { IsISO8601, IsNumber, IsOptional, ValidateNested, IsString, } from 'class-validator';
13
+ class ErrorOutput {
14
+ }
15
+ __decorate([
16
+ IsString(),
17
+ __metadata("design:type", String)
18
+ ], ErrorOutput.prototype, "code", void 0);
19
+ __decorate([
20
+ IsString(),
21
+ __metadata("design:type", String)
22
+ ], ErrorOutput.prototype, "message", void 0);
23
+ __decorate([
24
+ IsString(),
25
+ __metadata("design:type", Object)
26
+ ], ErrorOutput.prototype, "details", void 0);
27
+ class MetadataOutput {
28
+ }
29
+ __decorate([
30
+ IsString(),
31
+ IsISO8601(),
32
+ __metadata("design:type", String)
33
+ ], MetadataOutput.prototype, "serverTime", void 0);
34
+ __decorate([
35
+ Type(() => ErrorOutput),
36
+ ValidateNested(),
37
+ __metadata("design:type", ErrorOutput)
38
+ ], MetadataOutput.prototype, "error", void 0);
39
+ __decorate([
40
+ IsNumber(),
41
+ IsOptional(),
42
+ __metadata("design:type", Number)
43
+ ], MetadataOutput.prototype, "page", void 0);
44
+ __decorate([
45
+ IsNumber(),
46
+ IsOptional(),
47
+ __metadata("design:type", Number)
48
+ ], MetadataOutput.prototype, "limit", void 0);
49
+ __decorate([
50
+ IsNumber(),
51
+ IsOptional(),
52
+ __metadata("design:type", Number)
53
+ ], MetadataOutput.prototype, "total", void 0);
54
+ export class APIOutput extends TakaroDTO {
55
+ }
56
+ __decorate([
57
+ Type(() => MetadataOutput),
58
+ ValidateNested(),
59
+ __metadata("design:type", MetadataOutput)
60
+ ], APIOutput.prototype, "meta", void 0);
61
+ export function apiResponse(data = {}, opts) {
62
+ const returnVal = new APIOutput();
63
+ returnVal.meta = new MetadataOutput();
64
+ returnVal.data = {};
65
+ if (opts?.error) {
66
+ returnVal.meta.error = new ErrorOutput();
67
+ returnVal.meta.error.code = String(opts.error.name);
68
+ if ('details' in opts.error) {
69
+ if (opts.error instanceof errors.ValidationError) {
70
+ returnVal.meta.error.details = opts.error.details;
71
+ }
72
+ else {
73
+ returnVal.meta.error.details = opts.error.details;
74
+ }
75
+ }
76
+ returnVal.meta.error.message = String(opts.error.message);
77
+ }
78
+ if (opts?.meta) {
79
+ returnVal.meta.page = opts?.res.locals.page;
80
+ returnVal.meta.limit = opts?.res.locals.limit;
81
+ returnVal.meta.total = opts?.meta.total;
82
+ }
83
+ if (isTakaroDTO(data)) {
84
+ returnVal.data = data.toJSON();
85
+ }
86
+ else if (Array.isArray(data)) {
87
+ returnVal.data = data.map((item) => {
88
+ if (isTakaroDTO(item)) {
89
+ return item.toJSON();
90
+ }
91
+ return item;
92
+ });
93
+ }
94
+ else {
95
+ returnVal.data = data;
96
+ }
97
+ return returnVal;
98
+ }
99
+ //# sourceMappingURL=apiResponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiResponse.js","sourceRoot":"","sources":["../../src/util/apiResponse.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@takaro/http",
3
+ "version": "0.0.0-next.0da151e",
4
+ "description": "An opinionated http server",
5
+ "main": "dist/main.js",
6
+ "types": "dist/main.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "start:dev": "tsc --watch --preserveWatchOutput -p ./tsconfig.build.json",
10
+ "build": "tsc -p ./tsconfig.build.json"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC"
15
+ }
package/src/app.ts ADDED
@@ -0,0 +1,105 @@
1
+ import 'reflect-metadata';
2
+ import express, { Application } from 'express';
3
+ import { logger, errors, Sentry } from '@takaro/util';
4
+ import { Server, createServer } from 'node:http';
5
+ import { RoutingControllersOptions, useExpressServer } from 'routing-controllers';
6
+ import { Meta } from './controllers/meta.js';
7
+ import { LoggingMiddleware } from './middleware/logger.js';
8
+ import { ErrorHandler } from './middleware/errorHandler.js';
9
+ import bodyParser from 'body-parser';
10
+ import cors from 'cors';
11
+ import cookieParser from 'cookie-parser';
12
+ import { metricsMiddleware } from './main.js';
13
+ import { paginationMiddleware } from './middleware/paginationMiddleware.js';
14
+
15
+ interface IHTTPOptions {
16
+ port?: number;
17
+ allowedOrigins?: string[];
18
+ }
19
+
20
+ export class HTTP {
21
+ private app: Application;
22
+ private httpServer: Server;
23
+ private logger;
24
+
25
+ constructor(
26
+ options: RoutingControllersOptions = {},
27
+ private httpOptions: IHTTPOptions = {},
28
+ ) {
29
+ this.logger = logger('http');
30
+ this.app = express();
31
+ this.httpServer = createServer(this.app);
32
+ this.app.use(Sentry.Handlers.requestHandler());
33
+ this.app.use(
34
+ bodyParser.json({
35
+ limit: '1mb',
36
+ verify: (req, res, buf) => {
37
+ (req as any).rawBody = buf.toString();
38
+ },
39
+ }),
40
+ );
41
+ this.app.use(LoggingMiddleware);
42
+ this.app.use(metricsMiddleware);
43
+ this.app.use(paginationMiddleware);
44
+ this.app.set('x-powered-by', false);
45
+ this.app.use(
46
+ cors({
47
+ credentials: true,
48
+ exposedHeaders: ['X-Trace-Id'],
49
+ origin: (origin: string | undefined, callback: CallableFunction) => {
50
+ if (!origin) return callback(null, true);
51
+ const allowedOrigins = this.httpOptions.allowedOrigins ?? [];
52
+ if (!origin || allowedOrigins.includes(origin)) {
53
+ callback(null, true);
54
+ } else {
55
+ this.logger.warn(`Origin ${origin} not allowed by CORS`);
56
+ callback(new errors.BadRequestError('Not allowed by CORS'));
57
+ }
58
+ },
59
+ }),
60
+ );
61
+ this.app.use(cookieParser());
62
+
63
+ if (options.controllers) {
64
+ useExpressServer(this.app, {
65
+ ...options,
66
+ defaultErrorHandler: false,
67
+ validation: { whitelist: true, forbidNonWhitelisted: true },
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
69
+ controllers: [Meta, ...(options.controllers as Function[])],
70
+ });
71
+ } else {
72
+ useExpressServer(this.app, {
73
+ ...options,
74
+ defaultErrorHandler: false,
75
+ controllers: [Meta],
76
+ validation: { whitelist: true, forbidNonWhitelisted: true },
77
+ });
78
+ }
79
+
80
+ this.app.use(Sentry.Handlers.errorHandler());
81
+
82
+ this.app.use(ErrorHandler);
83
+ }
84
+
85
+ get expressInstance() {
86
+ return this.app;
87
+ }
88
+
89
+ get server(): Server {
90
+ return this.httpServer;
91
+ }
92
+
93
+ async start() {
94
+ this.httpServer = this.httpServer.listen(this.httpOptions.port, () => {
95
+ this.logger.info(`HTTP server listening on port ${this.httpOptions.port}`);
96
+ });
97
+ }
98
+
99
+ async stop() {
100
+ if (this.httpServer) {
101
+ this.httpServer.close();
102
+ this.logger.info('HTTP server stopped');
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,23 @@
1
+ import { HTTP } from '../../app.js';
2
+ import supertest from 'supertest';
3
+ import { describe, it, before, after } from 'node:test';
4
+
5
+ describe('app', () => {
6
+ let http: HTTP;
7
+ before(async () => {
8
+ http = new HTTP({}, { port: undefined });
9
+ await http.start();
10
+ });
11
+
12
+ after(async () => {
13
+ await http.stop();
14
+ });
15
+
16
+ it('Serves a health status', async () => {
17
+ await supertest(http.expressInstance).get('/healthz').expect(200);
18
+ });
19
+
20
+ it('Serves a open api spec', async () => {
21
+ await supertest(http.expressInstance).get('/openapi.json').expect(200);
22
+ });
23
+ });
@@ -0,0 +1,5 @@
1
+ declare module 'class-transformer/cjs/storage.js' {
2
+ import type { MetadataStorage } from 'class-transformer/types/MetadataStorage';
3
+
4
+ export const defaultMetadataStorage: MetadataStorage;
5
+ }