codeweaver 3.1.3 → 4.0.1

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 (79) hide show
  1. package/README.md +56 -73
  2. package/package.json +23 -1
  3. package/src/config.ts +17 -15
  4. package/src/constants.ts +1 -0
  5. package/src/core/aws/api-gateway.ts +187 -0
  6. package/src/core/aws/basic-types.ts +147 -0
  7. package/src/core/aws/dynamodb.ts +187 -0
  8. package/src/core/aws/index.ts +9 -0
  9. package/src/core/aws/lambda.ts +199 -0
  10. package/src/core/aws/message-broker.ts +167 -0
  11. package/src/core/aws/message.ts +259 -0
  12. package/src/core/aws/s3.ts +136 -0
  13. package/src/core/aws/utilities.ts +44 -0
  14. package/src/core/cache/basic-types.ts +17 -0
  15. package/src/core/cache/decorator.ts +72 -0
  16. package/src/core/cache/index.ts +4 -0
  17. package/src/core/cache/memory-cache.class.ts +119 -0
  18. package/src/{utilities/cache/redis-cache.ts → core/cache/redis-cache.class.ts} +58 -10
  19. package/src/core/container/basic-types.ts +10 -0
  20. package/src/{utilities → core/container}/container.ts +7 -17
  21. package/src/core/container/index.ts +2 -0
  22. package/src/{utilities → core/error}/error-handling.ts +1 -65
  23. package/src/core/error/index.ts +3 -0
  24. package/src/core/error/response-error.ts +45 -0
  25. package/src/core/error/send-http-error.ts +15 -0
  26. package/src/core/file/file-helpers.ts +166 -0
  27. package/src/core/file/index.ts +1 -0
  28. package/src/{utilities → core/helpers}/assignment.ts +2 -2
  29. package/src/core/helpers/comparison.ts +86 -0
  30. package/src/{utilities → core/helpers}/conversion.ts +2 -2
  31. package/src/core/helpers/decorators.ts +316 -0
  32. package/src/core/helpers/format.ts +9 -0
  33. package/src/core/helpers/index.ts +7 -0
  34. package/src/core/helpers/range.ts +67 -0
  35. package/src/core/helpers/types.ts +3 -0
  36. package/src/core/logger/index.ts +4 -0
  37. package/src/{utilities/logger/logger.config.ts → core/logger/winston-logger.config.ts} +1 -1
  38. package/src/{utilities → core}/logger/winston-logger.service.ts +3 -3
  39. package/src/core/message-broker/bullmq/basic-types.ts +67 -0
  40. package/src/core/message-broker/bullmq/broker.ts +141 -0
  41. package/src/core/message-broker/bullmq/index.ts +3 -0
  42. package/src/core/message-broker/bullmq/queue.ts +58 -0
  43. package/src/core/message-broker/bullmq/worker.ts +68 -0
  44. package/src/core/message-broker/kafka/basic-types.ts +45 -0
  45. package/src/core/message-broker/kafka/consumer.ts +95 -0
  46. package/src/core/message-broker/kafka/index.ts +3 -0
  47. package/src/core/message-broker/kafka/producer.ts +113 -0
  48. package/src/core/message-broker/rabitmq/basic-types.ts +44 -0
  49. package/src/core/message-broker/rabitmq/channel.ts +95 -0
  50. package/src/core/message-broker/rabitmq/consumer.ts +94 -0
  51. package/src/core/message-broker/rabitmq/index.ts +4 -0
  52. package/src/core/message-broker/rabitmq/producer.ts +100 -0
  53. package/src/core/message-broker/utilities.ts +50 -0
  54. package/src/core/middlewares/basic-types.ts +39 -0
  55. package/src/core/middlewares/decorators.ts +244 -0
  56. package/src/core/middlewares/index.ts +3 -0
  57. package/src/core/middlewares/middlewares.ts +246 -0
  58. package/src/core/parallel/index.ts +3 -0
  59. package/src/{utilities → core}/parallel/parallel.ts +11 -1
  60. package/src/core/rate-limit/basic-types.ts +43 -0
  61. package/src/core/rate-limit/index.ts +4 -0
  62. package/src/core/rate-limit/memory-store.ts +65 -0
  63. package/src/core/rate-limit/rate-limit.ts +134 -0
  64. package/src/core/rate-limit/redis-store.ts +141 -0
  65. package/src/core/retry/basic-types.ts +21 -0
  66. package/src/core/retry/decorator.ts +139 -0
  67. package/src/core/retry/index.ts +2 -0
  68. package/src/main.ts +6 -8
  69. package/src/routers/orders/index.router.ts +5 -1
  70. package/src/routers/orders/order.controller.ts +54 -64
  71. package/src/routers/products/index.router.ts +2 -1
  72. package/src/routers/products/product.controller.ts +33 -68
  73. package/src/routers/users/index.router.ts +1 -1
  74. package/src/routers/users/user.controller.ts +25 -50
  75. package/src/utilities/cache/memory-cache.ts +0 -74
  76. /package/src/{utilities → core}/logger/base-logger.interface.ts +0 -0
  77. /package/src/{utilities → core}/logger/logger.service.ts +0 -0
  78. /package/src/{utilities → core}/parallel/chanel.ts +0 -0
  79. /package/src/{utilities → core}/parallel/worker-pool.ts +0 -0
@@ -0,0 +1,100 @@
1
+ import { ChannelManager } from "./channel";
2
+ import { ProducerConfig } from "./basic-types";
3
+ import { backoff } from "../utilities";
4
+
5
+ /**
6
+ * RabbitMQ producer.
7
+ * @template T - The type of the message body.
8
+ */
9
+ export class RabbitProducer<T = any> {
10
+ private cm = new ChannelManager({ url: "" });
11
+ private config: ProducerConfig;
12
+
13
+ /**
14
+ * Creates a new RabbitMQ producer with the given configuration options.
15
+ * @param opts - Configuration options for the producer.
16
+ */
17
+ public constructor(opts: ProducerConfig) {
18
+ this.config = opts;
19
+ this.cm = new ChannelManager({ url: opts.url });
20
+ }
21
+
22
+ /**
23
+ * Connects to the RabbitMQ cluster and sets up the producer.
24
+ * Ensures the exchange exists.
25
+ * @returns A promise resolved when the producer is connected.
26
+ */
27
+ public async connect(): Promise<void> {
28
+ await this.cm.connect();
29
+ const ex = this.config.exchange ?? "";
30
+ if (ex) {
31
+ const type = this.config.exchangeType ?? "direct";
32
+ await this.cm.assertExchange(ex, type, true);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Publishes a message to a RabbitMQ exchange.
38
+ * If a serializer is provided in the producer config, it will be used to serialize the message body.
39
+ * Otherwise, the message body will be serialized as JSON.
40
+ * @param message - The message to publish with optional routing key and headers.
41
+ * @returns A promise resolved when the message has been published.
42
+ */
43
+ public async publish(message: {
44
+ body: T;
45
+ routingKey?: string;
46
+ headers?: any;
47
+ }): Promise<void> {
48
+ if (!message) throw new Error("Message required");
49
+ if (!this.cm) throw new Error("ChannelManager not initialized");
50
+
51
+ const bodyBuf = this.config.serializer?.serialize
52
+ ? this.config.serializer.serialize(message.body)
53
+ : Buffer.from(JSON.stringify(message.body));
54
+
55
+ const routingKey = message.routingKey ?? this.config.routingKey ?? "";
56
+ const exchange = this.config.exchange ?? "";
57
+
58
+ // Ensure connection and exchange
59
+ if (!this.cm) await this.connect();
60
+
61
+ const payload = {
62
+ exchange,
63
+ routingKey,
64
+ persistent: true,
65
+ contentType: "application/json",
66
+ payload: bodyBuf,
67
+ headers: message.headers,
68
+ } as any;
69
+
70
+ // kaf way: use channel.publish
71
+ let attempt = 0;
72
+ while (true) {
73
+ try {
74
+ await this.cm.ch.publish(exchange, routingKey, bodyBuf, {
75
+ headers: message.headers,
76
+ persistent: true,
77
+ });
78
+ return;
79
+ } catch (e) {
80
+ attempt++;
81
+ const max = this.config.retry?.retries ?? 5;
82
+ if (attempt > max) throw e;
83
+ const wait = backoff(
84
+ attempt,
85
+ this.config.retry?.baseMs ?? 100,
86
+ this.config.retry?.maxMs ?? 2000
87
+ );
88
+ await new Promise((r) => setTimeout(r, wait));
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Closes the underlying ChannelManager and its associated objects.
95
+ * @returns A promise resolved when all objects have been closed.
96
+ */
97
+ public async close(): Promise<void> {
98
+ await this.cm.close();
99
+ }
100
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Compute the next delay in milliseconds according to an exponential backoff
3
+ * algorithm with jitter.
4
+ *
5
+ * @param attempt The attempt number, starting from 1.
6
+ * @param baseMs The base delay in milliseconds, defaults to 100.
7
+ * @param maxMs The maximum delay in milliseconds, defaults to 3000.
8
+ * @returns The computed delay in milliseconds.
9
+ */
10
+ export function backoff(attempt: number, baseMs = 100, maxMs = 3000): number {
11
+ const cap = Math.min(maxMs, baseMs * Math.pow(2, attempt));
12
+ // jitter
13
+ return cap / 2 + Math.floor(Math.random() * (cap / 2));
14
+ }
15
+
16
+ /**
17
+ * Validates a topic name.
18
+ * @param name The topic name to validate.
19
+ * @throws {Error} If the topic name is invalid (null, undefined, not a string) or too long (> 249 characters).
20
+ */
21
+ export function validateTopic(name: string): void {
22
+ if (!name || typeof name !== "string") throw new Error("Invalid topic name");
23
+ // simple rule demo; adapt to your needs
24
+ if (name.length > 249) throw new Error("Topic name too long");
25
+ }
26
+
27
+ /**
28
+ * Interface for serializing and deserializing data for a message broker.
29
+ * @template T The type of data to serialize and deserialize.
30
+ */
31
+ export interface BrokerSerializer<T = any> {
32
+ version?: number;
33
+ serialize?: (data: T) => Buffer;
34
+ deserialize?: (buffer: Buffer) => T;
35
+ }
36
+
37
+ /**
38
+ * Returns a default broker serializer for JSON-serialized data.
39
+ * The serializer will store the given version number in the serialized data.
40
+ * The deserializer will parse the JSON data and return the deserialized object.
41
+ * @param version The version number to store in the serialized data.
42
+ * @returns A default broker serializer for JSON-serialized data.
43
+ */
44
+ export function getDefaultJsonSchema<T>(version?: number): BrokerSerializer<T> {
45
+ return {
46
+ version,
47
+ serialize: (data: T): Buffer => Buffer.from(JSON.stringify(data)),
48
+ deserialize: (buffer: Buffer): T => JSON.parse(buffer.toString()) as T,
49
+ };
50
+ }
@@ -0,0 +1,39 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+
3
+ /**
4
+ * Extend Express Request to optionally include a user payload.
5
+ * This allows downstream middleware and handlers to access authenticated user data.
6
+ */
7
+ export interface UserRequest<UserIdType = string | number> extends Request {
8
+ /** Authenticated user payload parsed from the JWT. */
9
+ user?: UserPayload<UserIdType>;
10
+ }
11
+
12
+ export type RouterFn<UserIdType = string | number> = (
13
+ req: UserRequest<UserIdType>,
14
+ res: Response,
15
+ next: NextFunction
16
+ ) => void;
17
+
18
+ /**
19
+ * User payload attached to the request after successful authentication.
20
+ * Extends JwtPayload with a flexible key-value store.
21
+ */
22
+ export type UserPayload<UserIdType = string | number> = {
23
+ id?: UserIdType;
24
+ } & Record<string, any>;
25
+
26
+ /**
27
+ * Options for the authenticateToken middleware.
28
+ * - audience: Optional JWT audience claim to validate against.
29
+ * - issuer: Optional JWT issuer claim to validate against.
30
+ * - maxAge: Optional maximum age for the token (e.g., "1h", "15m").
31
+ */
32
+ export type JwtOptions = {
33
+ /** Valid audience for the JWT. */
34
+ audience?: string;
35
+ /** Valid issuer for the JWT. */
36
+ issuer?: string;
37
+ /** Maximum allowed age for the JWT (e.g., "1h"). */
38
+ maxAge?: string;
39
+ };
@@ -0,0 +1,244 @@
1
+ import { logger } from "@/core/logger";
2
+ import { ResponseError } from "@/core/error";
3
+ import jwt from "jsonwebtoken";
4
+ import { config } from "@/config";
5
+ import { JwtOptions, UserPayload, UserRequest } from "./basic-types";
6
+ import { createMethodDecorator } from "@/core/helpers/decorators";
7
+ import { AsyncFn } from "@/core/helpers";
8
+ import { authenticate, hashRequest, inFlightMap } from "./middlewares";
9
+ import { AuthenticateCallback, AuthenticateOptions, Strategy } from "passport";
10
+
11
+ /**
12
+ * Decorator: Log a request when the controller method is invoked.
13
+ */
14
+ export const LogRequest = createMethodDecorator<[], [UserRequest]>(
15
+ async ([], [req]) => {
16
+ const { method, url } = req;
17
+ logger.info(
18
+ `Request Method=${method}, Url=${url}, User Id=${req.user?.id}, IP=${req.ip}, Agent=${req.headers["user-agent"]}`,
19
+ "Request",
20
+ {
21
+ user: req.user,
22
+ body: req.body,
23
+ query: req.query,
24
+ params: req.params,
25
+ headers: req.headers,
26
+ cookies: req.cookies,
27
+ }
28
+ );
29
+ return null;
30
+ }
31
+ );
32
+
33
+ /**
34
+ * Decorator: Log a method invocation with arguments and result.
35
+ */
36
+ export const LogMethod = createMethodDecorator<[], [UserRequest]>(
37
+ async ([], [], method, args, classInstance) => {
38
+ const startTime = process.hrtime.bigint();
39
+ const result = await method.apply(classInstance, args);
40
+ const endTime = process.hrtime.bigint();
41
+ const duration = Number(endTime - startTime) / 1000000;
42
+ logger.info("Method arguments and result", "Method", {
43
+ args,
44
+ result,
45
+ duration,
46
+ reqPerSec: 1000 / duration,
47
+ });
48
+ return result;
49
+ }
50
+ );
51
+
52
+ /**
53
+ * Decorator: Authenticate token and attach user to req.
54
+ * Internally uses the existing authenticateToken middleware logic without changing route signatures.
55
+ */
56
+ export const AuthenticateToken = createMethodDecorator<
57
+ [JwtOptions | undefined],
58
+ [UserRequest]
59
+ >(async ([options], [req]) => {
60
+ const tokenHeader = req.header("Authorization");
61
+ const token = tokenHeader?.split(" ")[1];
62
+ if (token == null) {
63
+ throw new ResponseError("Authentication failed: Token not found.", 401);
64
+ }
65
+
66
+ try {
67
+ const decoded = jwt.verify(token, config.jwtSecretKey, options);
68
+ req.user = decoded as UserPayload;
69
+ return null;
70
+ } catch (error: unknown) {
71
+ let message = "";
72
+ if (error instanceof Error) message = error.message;
73
+ throw new ResponseError(
74
+ "Authentication failed: Token is invalid.",
75
+ 401,
76
+ message
77
+ );
78
+ }
79
+ });
80
+
81
+ /**
82
+ * Decorator: Apply authentication using passport.authenticate
83
+ * Internally uses the existing authenticatePassport middleware logic without changing route signatures.
84
+ */
85
+ export const Authenticate = createMethodDecorator<
86
+ [
87
+ string | string[] | Strategy,
88
+ AuthenticateOptions | undefined,
89
+ AuthenticateCallback | ((...args: any[]) => Promise<any>) | undefined,
90
+ (
91
+ | ((
92
+ req: UserRequest,
93
+ strategy: string | string[] | Strategy,
94
+ options: AuthenticateOptions,
95
+ callback?: AuthenticateCallback | ((...args: any[]) => Promise<any>)
96
+ ) => any)
97
+ | undefined
98
+ )
99
+ ],
100
+ [UserRequest]
101
+ >(async ([strategy, options, callback, authenticationFn], [req]) => {
102
+ await (authenticationFn ?? authenticate)(
103
+ req,
104
+ strategy,
105
+ options ?? {},
106
+ callback
107
+ );
108
+
109
+ if (req.user?.id === null) {
110
+ throw new ResponseError("Authentication failed.", 401);
111
+ }
112
+
113
+ return null;
114
+ });
115
+
116
+ /**
117
+ * Decorator: Apply a timeout to the controller method.
118
+ * Mirrors the timeout middleware but applied at method level.
119
+ */
120
+ export function Timeout(
121
+ milliseconds: number,
122
+ timeoutHandler?: () => Promise<any>
123
+ ) {
124
+ return createMethodDecorator<[number, (() => Promise<any>) | undefined], []>(
125
+ async ([milliseconds, timeoutHandler], [], method, args, classInstance) => {
126
+ const controller = new AbortController();
127
+
128
+ // Create a promise that rejects after the timeout
129
+ let timeoutHandle: NodeJS.Timeout;
130
+ const timeoutPromise = new Promise<any>(async (resolve, reject) => {
131
+ timeoutHandle = setTimeout(() => {
132
+ if (timeoutHandler != null) {
133
+ resolve(timeoutHandler());
134
+ } else {
135
+ controller.abort();
136
+ reject(new ResponseError("Request timed out", 503));
137
+ }
138
+ }, milliseconds);
139
+ });
140
+
141
+ // Add the signal to the method arguments
142
+ args.push(controller.signal);
143
+
144
+ // Run the original method
145
+ try {
146
+ return await Promise.race([
147
+ timeoutPromise,
148
+ method.apply(classInstance, args),
149
+ ]);
150
+ } finally {
151
+ clearTimeout(timeoutHandle!);
152
+ }
153
+ }
154
+ )(milliseconds, timeoutHandler);
155
+ }
156
+
157
+ /**
158
+ * Decorator: Per-user debounce for a controller method.
159
+ * Mirrors perUserDebounce with optional cleanup after response.
160
+ */
161
+ export const Debounce = createMethodDecorator<
162
+ [
163
+ number | undefined,
164
+ Map<string, number> | undefined,
165
+ ((req: UserRequest) => string) | undefined
166
+ ],
167
+ [req: UserRequest]
168
+ >(async ([debounceTimeSpan, inFlight, hashFunction], [req]) => {
169
+ // Import the in-flight mechanism from your existing module
170
+ // If inFlight and hashRequest are exported, import them here
171
+ const userId = req.user?.id ?? "anonymous";
172
+ const key =
173
+ `${userId}:` +
174
+ (hashFunction != null ? hashFunction(req) : hashRequest(req));
175
+
176
+ const now = Date.now();
177
+ const last = (inFlight ?? inFlightMap).get(key) ?? 0;
178
+
179
+ if (now - last < (debounceTimeSpan ?? config.debounceTimeSpan)) {
180
+ throw new ResponseError("Please wait before repeating this action.", 429);
181
+ }
182
+
183
+ (inFlight ?? inFlightMap).set(key, now);
184
+ return null;
185
+ });
186
+
187
+ /**
188
+ * Before decorator: run a function before the controller method.
189
+ * You can supply a small hook to run any pre-processing (e.g., logging, metric increment).
190
+ */
191
+ export const Before = createMethodDecorator<
192
+ [(method: AsyncFn, ...methodArgs: any[]) => Promise<boolean>],
193
+ any[]
194
+ >(async ([callback], _args, method, rawMethodArgs) => {
195
+ return await callback(method, rawMethodArgs);
196
+ });
197
+
198
+ /**
199
+ * After decorator: run a function after the controller method completes.
200
+ * If the controller method returns a Promise, the afterFn runs after it resolves.
201
+ */
202
+ export const After = createMethodDecorator<
203
+ [(result: any, method: AsyncFn, ...methodArgs: any[]) => Promise<void>],
204
+ []
205
+ >(undefined, async (result, [callback], _args, method, rawMethodArgs) => {
206
+ return await callback(result, method, rawMethodArgs);
207
+ });
208
+
209
+ /**
210
+ * Error handler decorator with a callback
211
+ * The callback receives the error and a context object describing the invocation.
212
+ *
213
+ * @param callback - callback invoked when an error occurs.
214
+ * @returns decorator
215
+ */
216
+ export const ErrorHandler = createMethodDecorator<
217
+ [(error: Error) => Promise<void>],
218
+ []
219
+ >(async ([callback], [], method, rawMethodArgs, classInstance) => {
220
+ try {
221
+ return await method.apply(classInstance, rawMethodArgs);
222
+ } catch (error: unknown) {
223
+ return await callback(
224
+ error instanceof Error ? (error as Error) : new Error("Unexpected error!")
225
+ );
226
+ }
227
+ });
228
+
229
+ /**
230
+ * Guard decorator: restrict access to a controller method.
231
+ *
232
+ * @param callback - callback invoked to check if the request is allowed: (req) => Promise<boolean>
233
+ * @returns decorator
234
+ */
235
+ export const Guard = createMethodDecorator<
236
+ [(req: UserRequest) => Promise<boolean>],
237
+ [UserRequest]
238
+ >(async ([callback], [req]) => {
239
+ const isAllowed = await callback(req);
240
+ if (!isAllowed) {
241
+ throw new ResponseError("Forbidden", 403);
242
+ }
243
+ return null;
244
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./basic-types";
2
+ export * from "./middlewares";
3
+ export * from "./decorators";
@@ -0,0 +1,246 @@
1
+ import { config } from "@/config";
2
+ import { Response, NextFunction } from "express";
3
+ import jwt from "jsonwebtoken";
4
+ import { ResponseError } from "@/core/error";
5
+ import { logger } from "@/core/logger";
6
+ import { JwtOptions, RouterFn, UserPayload, UserRequest } from "./basic-types";
7
+ import passport, {
8
+ AuthenticateCallback,
9
+ AuthenticateOptions,
10
+ Strategy,
11
+ } from "passport";
12
+
13
+ /**
14
+ * Simple request logger middleware.
15
+ * Logs the HTTP method and URL of each incoming request at the "Request" context.
16
+ *
17
+ * @param req - Express HTTP request object
18
+ * @param res - Express HTTP response object
19
+ * @param next - Next middleware function
20
+ */
21
+ export function requestLogger<
22
+ UserIdType = string | number
23
+ >(): RouterFn<UserIdType> {
24
+ return async (req, _res, next) => {
25
+ const { method, url } = req;
26
+ logger.info(
27
+ `Method=${method}, Url=${url}, User Id=${req.user?.id}, IP=${req.ip}, Agent=${req.headers["user-agent"]}`,
28
+ "Request",
29
+ {
30
+ user: req.user,
31
+ body: req.body,
32
+ query: req.query,
33
+ params: req.params,
34
+ headers: req.headers,
35
+ cookies: req.cookies,
36
+ }
37
+ );
38
+ next();
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Factory to create a middleware that authenticates a JWT from the Authorization header.
44
+ * Validates the token using the configured secret key and optional JWT claims.
45
+ *
46
+ * Behavior:
47
+ * - Extracts the token from the Authorization header (Bearer <token>).
48
+ * - Verifies the token with jwt.verify using config.jwtSecretKey and optional options.
49
+ * - Attaches the decoded payload as req.user.
50
+ * - Calls await next() on success.
51
+ * - Throws a 401 ResponseError if token is missing or invalid.
52
+ *
53
+ * @param options Optional JWT validation constraints.
54
+ * @returns Express middleware function.
55
+ */
56
+ export function authenticateToken<UserIdType = string | number>(
57
+ options: JwtOptions
58
+ ): RouterFn<UserIdType> {
59
+ return async (req, _res, next) => {
60
+ let authHeader = req.header("Authorization");
61
+ const token = authHeader?.split(" ")[1];
62
+
63
+ if (token == null) {
64
+ throw new ResponseError("Authentication failed: Token not found.", 401);
65
+ }
66
+
67
+ try {
68
+ const decoded = jwt.verify(token, config.jwtSecretKey, options);
69
+ req.user = decoded as UserPayload<UserIdType>;
70
+ next();
71
+ } catch {
72
+ throw new ResponseError("Authentication failed: Token is invalid.", 401);
73
+ }
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Utility function to authenticate a user using passport.authenticate.
79
+ * Throws a 401 ResponseError if authentication fails.
80
+ *
81
+ * @param req - Express HTTP request object
82
+ * @param strategy - Passport strategy to use for authentication
83
+ * @param options - Optional authentication options
84
+ * @param callback - Optional callback function
85
+ * @returns Express middleware function
86
+ */
87
+ export function authenticate<UserIdType = string | number>(
88
+ req: UserRequest<UserIdType>,
89
+ strategy: string | string[] | Strategy,
90
+ options: AuthenticateOptions,
91
+ callback?: AuthenticateCallback | ((...args: any[]) => any)
92
+ ) {
93
+ passport.authenticate(
94
+ strategy,
95
+ options,
96
+ async (
97
+ error: any,
98
+ user?: Express.User | false | null,
99
+ info?: object | string | Array<string | undefined>,
100
+ status?: number | Array<number | undefined>
101
+ ) => {
102
+ if (error) throw new ResponseError("Authentication failed.", 401, error);
103
+ if (!user) throw new ResponseError("Authentication failed.", 401);
104
+ await callback?.(error, user, info, status);
105
+ req.user = user;
106
+ }
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Middleware to authenticate a user using passport.authenticate.
112
+ * Throws a 401 ResponseError if authentication fails.
113
+ *
114
+ * @param strategy - Passport strategy to use for authentication
115
+ * @param options - Optional authentication options
116
+ * @param callback - Optional callback function
117
+ * @returns Express middleware function
118
+ */
119
+ export function authenticatePassport<UserIdType = string | number>(
120
+ strategy: string | string[] | Strategy,
121
+ options?: AuthenticateOptions,
122
+ callback?: AuthenticateCallback | ((...args: any[]) => any),
123
+ authenticationFn?: (
124
+ req: UserRequest<UserIdType>,
125
+ strategy: string | string[] | Strategy,
126
+ options: AuthenticateOptions,
127
+ callback?: AuthenticateCallback | ((...args: any[]) => any)
128
+ ) => any
129
+ ): RouterFn<UserIdType> {
130
+ return async (req: UserRequest<UserIdType>, _res, next) => {
131
+ await (authenticationFn ?? authenticate)(
132
+ req,
133
+ strategy,
134
+ options ?? {},
135
+ callback
136
+ );
137
+ next();
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Timeout middleware to enforce request time limits.
143
+ * Starts a timer for the given duration and, if the response has not finished
144
+ * by then, responds with HTTP 503 (Request timed out).
145
+ * When the response finishes, invokes onFinish with the timer reference and clears it.
146
+ *
147
+ * @param milliseconds Duration before timing out the request (in ms)
148
+ * @param onFinish Callback invoked when the response finishes, receiving the timer
149
+ * @returns Express middleware function
150
+ */
151
+ export function timeout(
152
+ milliseconds: number,
153
+ onFinish: (timer: NodeJS.Timeout) => void
154
+ ): RouterFn {
155
+ return async (_req, res, next) => {
156
+ const timer = setTimeout(() => {
157
+ if (!res.headersSent) {
158
+ res.status(503).json({ message: "Request timed out" });
159
+ }
160
+ }, milliseconds);
161
+
162
+ res.once("finish", () => {
163
+ onFinish(timer);
164
+ clearTimeout(timer);
165
+ });
166
+ res.once("close", () => clearTimeout(timer));
167
+ next();
168
+ };
169
+ }
170
+
171
+ // Map of in-flight requests per user
172
+ export const inFlightMap: Map<string, number> = new Map();
173
+
174
+ /**
175
+ * Utility function to generate a hash for a request.
176
+ * The hash is based on the method, URL and body of the request.
177
+ *
178
+ * @param req - Express HTTP request object
179
+ * @returns A string representation of the hash
180
+ */
181
+ export function hashRequest<UserIdType = string | number>(
182
+ req: UserRequest<UserIdType>
183
+ ): string {
184
+ const { method, url, body } = req;
185
+ const payload = JSON.stringify(body ?? {});
186
+ const str = `${method}:${url}:${payload}`;
187
+ let h = 0;
188
+ for (let i = 0; i < str.length; i++) {
189
+ h = (h << 5) - h + str.charCodeAt(i);
190
+ h |= 0;
191
+ }
192
+ return String(h);
193
+ }
194
+
195
+ /**
196
+ * Per-user in-flight request debounce.
197
+ * Prevents identical in-flight requests from being processed concurrently within a
198
+ * short time span by the same user.
199
+ *
200
+ * - Uses a lightweight hash of the request (method, url, body) together with a per-user key.
201
+ * - If a similar request was processed within config.debounceTimeSpan, responds with 429
202
+ * and a Retry-After header indicating the wait time.
203
+ * - Stores the timestamp of the last in-flight occurrence in a map.
204
+ * - Optionally cleans up the in-flight key after the response finishes if
205
+ * cleanUpAfterResponse is true.
206
+ *
207
+ * @param cleanUpAfterResponse If true, cleans up the in-flight key after the response finishes.
208
+ * @returns Express middleware function
209
+ */
210
+ export function debounce<UserIdType = string | number>(
211
+ cleanUpAfterResponse: boolean = false,
212
+ inFlight: Map<string, number> = inFlightMap,
213
+ hashFunction: (req: UserRequest<UserIdType>) => string = hashRequest
214
+ ) {
215
+ return async (
216
+ req: UserRequest<UserIdType>,
217
+ res: Response,
218
+ next: NextFunction
219
+ ) => {
220
+ const userId = req.user?.id ?? "anonymous";
221
+ const key = `${userId}:${hashFunction(req)}`;
222
+
223
+ const now = Date.now();
224
+ const last = inFlight.get(key) ?? 0;
225
+
226
+ if (now - last < config.debounceTimeSpan) {
227
+ res.header(
228
+ "Retry-After",
229
+ String(Math.ceil((config.debounceTimeSpan - (now - last)) / 1000))
230
+ );
231
+ return res
232
+ .status(429)
233
+ .json({ error: "Please wait before repeating this action." });
234
+ }
235
+
236
+ inFlight.set(key, now);
237
+
238
+ if (cleanUpAfterResponse) {
239
+ res.on("finish", () => {
240
+ inFlight.delete(key);
241
+ });
242
+ }
243
+
244
+ next();
245
+ };
246
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./chanel";
2
+ export * from "./worker-pool";
3
+ export * from "./parallel";