nodecore-kit 0.3.0 → 0.4.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/dist/index.d.ts CHANGED
@@ -1,15 +1,76 @@
1
- import RedisClient, { RedisOptions } from 'ioredis';
1
+ import { AxiosProgressEvent } from 'axios';
2
+ import { Request, Response, NextFunction } from 'express';
3
+ import { Schema, ValidationOptions } from 'joi';
4
+ import { RedisOptions } from 'ioredis';
5
+ import { MessageAttributeValue } from '@aws-sdk/client-sqs';
6
+ import { ObjectCannedACL } from '@aws-sdk/client-s3';
7
+ import { Readable } from 'stream';
2
8
  import jwt, { SignOptions } from 'jsonwebtoken';
3
9
 
4
- declare const makeRequest: ({ url, method, headers, token, data, }: {
10
+ type HttpMethod = "GET" | "DELETE" | "POST" | "PATCH" | "PUT";
11
+ interface RequestOptions<TData = unknown> {
5
12
  url: string;
6
- method?: "GET" | "DELETE" | "POST" | "PATCH" | "PUT";
7
- headers?: Record<string, any>;
13
+ method?: HttpMethod;
14
+ headers?: Record<string, string>;
15
+ /** Auth token — will be sent as `Bearer <token>` */
8
16
  token?: string;
9
- data?: Record<string, any>;
10
- }) => Promise<Record<string, any>>;
17
+ /** Request body — plain object, array, or FormData */
18
+ data?: TData;
19
+ /** Query string parameters */
20
+ params?: Record<string, any>;
21
+ /** Request timeout in milliseconds (default: 10_000) */
22
+ timeout?: number;
23
+ /** Number of retry attempts on network errors or 5xx responses (default: 0) */
24
+ retries?: number;
25
+ /** Upload/download progress callback */
26
+ onProgress?: (event: AxiosProgressEvent) => void;
27
+ }
28
+ /**
29
+ * Makes an HTTP request via Axios with consistent error handling,
30
+ * optional auth, query params, timeout, and retry support.
31
+ *
32
+ * @example
33
+ * const user = await makeRequest<User>({ url: "/api/user/1" });
34
+ * const post = await makeRequest<Post>({ url: "/api/posts", method: "POST", data: { title: "Hello" } });
35
+ */
36
+ declare const makeRequest: <TResponse = Record<string, any>, TData = unknown>(options: RequestOptions<TData>, _retryCount?: number) => Promise<TResponse>;
11
37
 
12
- declare function joiValidator(constraint: any, isMiddleware?: boolean): any;
38
+ interface FieldConstraint {
39
+ schema: Schema;
40
+ options?: ValidationOptions;
41
+ }
42
+ interface JoiConstraints {
43
+ body?: FieldConstraint;
44
+ params?: FieldConstraint;
45
+ query?: FieldConstraint;
46
+ headers?: FieldConstraint;
47
+ files?: FieldConstraint;
48
+ }
49
+ interface DirectConstraint {
50
+ schema: Schema;
51
+ data: unknown;
52
+ options?: ValidationOptions;
53
+ }
54
+ /**
55
+ * Express middleware that validates req.body, params, query, headers, and/or files.
56
+ * Replaces each field with the sanitized, validated value from Joi.
57
+ *
58
+ * @example
59
+ * router.post("/users", joiMiddleware({
60
+ * body: { schema: createUserSchema },
61
+ * params: { schema: idParamSchema },
62
+ * }), createUser);
63
+ */
64
+ declare const joiMiddleware: (constraints: JoiConstraints) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
65
+ /**
66
+ * Validates data directly outside of a middleware context.
67
+ * Throws a ValidationError if invalid.
68
+ *
69
+ * @example
70
+ * const value = joiValidate({ schema: createUserSchema, data: req.body });
71
+ * const typed = joiValidate<CreateUserDto>({ schema: createUserSchema, data: req.body });
72
+ */
73
+ declare const joiValidate: <T = unknown>({ schema, data, options, }: DirectConstraint) => T;
13
74
 
14
75
  declare const HTTP_STATUS: {
15
76
  readonly OK: {
@@ -64,6 +125,14 @@ declare const HTTP_STATUS: {
64
125
  type HttpStatusKey = keyof typeof HTTP_STATUS;
65
126
  type HttpStatus = (typeof HTTP_STATUS)[HttpStatusKey];
66
127
  declare const HTTP_STATUS_CODE_ERROR: Record<number, string>;
128
+ /**
129
+ * Base class for all application errors.
130
+ * Extends the native Error with an HTTP status code, status message,
131
+ * optional machine-readable error code, and optional metadata.
132
+ *
133
+ * @example
134
+ * throw new AppError(HTTP_STATUS.NOT_FOUND, "User not found", "NOT_FOUND");
135
+ */
67
136
  declare class AppError extends Error {
68
137
  readonly statusCode: number;
69
138
  readonly statusMessage: string;
@@ -71,36 +140,107 @@ declare class AppError extends Error {
71
140
  readonly meta?: Record<string, any>;
72
141
  constructor(status: HttpStatus, message?: string | null, errorCode?: string, meta?: Record<string, any>);
73
142
  }
143
+ /**
144
+ * 422 — request is well-formed but contains invalid field values.
145
+ *
146
+ * @example
147
+ * throw new ValidationError("Email is invalid");
148
+ * throw new ValidationError("Validation failed", { fields: ["email", "name"] });
149
+ */
74
150
  declare class ValidationError extends AppError {
75
151
  constructor(message?: string | null, meta?: Record<string, any>);
76
152
  }
153
+ /**
154
+ * 401 — caller is not authenticated.
155
+ *
156
+ * @example
157
+ * throw new AuthenticationError("Invalid credentials");
158
+ */
77
159
  declare class AuthenticationError extends AppError {
78
160
  constructor(message?: string | null, meta?: Record<string, any>);
79
161
  }
162
+ /**
163
+ * 403 — caller is authenticated but not permitted to access this resource.
164
+ *
165
+ * @example
166
+ * throw new AuthorizationError("You do not have permission to perform this action");
167
+ */
80
168
  declare class AuthorizationError extends AppError {
81
169
  constructor(message?: string | null, meta?: Record<string, any>);
82
170
  }
171
+ /**
172
+ * 404 — requested resource does not exist.
173
+ *
174
+ * @example
175
+ * throw new NotFoundError("User not found");
176
+ */
83
177
  declare class NotFoundError extends AppError {
84
178
  constructor(message?: string | null, meta?: Record<string, any>);
85
179
  }
180
+ /**
181
+ * 498 — token has passed its expiry time.
182
+ *
183
+ * @example
184
+ * throw new TokenExpiredError("Session has expired, please log in again");
185
+ */
86
186
  declare class TokenExpiredError extends AppError {
87
187
  constructor(message?: string | null, meta?: Record<string, any>);
88
188
  }
189
+ /**
190
+ * 499 — token is malformed or its signature is invalid.
191
+ *
192
+ * @example
193
+ * throw new TokenInvalidError("Token is invalid");
194
+ */
89
195
  declare class TokenInvalidError extends AppError {
90
196
  constructor(message?: string | null, meta?: Record<string, any>);
91
197
  }
198
+ /**
199
+ * 400 — request is malformed or missing required fields.
200
+ *
201
+ * @example
202
+ * throw new BadRequestError("Request body is missing required fields");
203
+ */
92
204
  declare class BadRequestError extends AppError {
93
205
  constructor(message?: string | null, meta?: Record<string, any>);
94
206
  }
207
+ /**
208
+ * 500 — unexpected server-side failure.
209
+ * Use `meta` to attach the original cause without leaking internals to the client.
210
+ *
211
+ * @example
212
+ * throw new ServerError("Failed to connect to database", { cause: err });
213
+ */
95
214
  declare class ServerError extends AppError {
96
215
  constructor(message?: string | null, meta?: Record<string, any>);
97
216
  }
217
+ /**
218
+ * 409 — resource already exists.
219
+ *
220
+ * @example
221
+ * throw new ExistingError("A user with this email already exists");
222
+ */
98
223
  declare class ExistingError extends AppError {
99
224
  constructor(message?: string | null, meta?: Record<string, any>);
100
225
  }
226
+ /**
227
+ * 204 — operation succeeded but there is no content to return.
228
+ *
229
+ * @example
230
+ * throw new NoContent();
231
+ */
101
232
  declare class NoContent extends AppError {
102
233
  constructor(message?: string | null, meta?: Record<string, any>);
103
234
  }
235
+ /**
236
+ * Framework-agnostic error normalizer.
237
+ * Converts any thrown value into a consistent response shape.
238
+ * Handles AppError, Axios errors, native Error, and plain strings.
239
+ *
240
+ * @example
241
+ * const response = errorHandler(err, "FATAL_ERROR", "auth-service");
242
+ * res.status(response.httpStatusCode).json(response);
243
+ */
104
244
  declare const errorHandler: (err: any, ERROR_TYPE?: string, service?: string) => {
105
245
  message: any;
106
246
  error: any;
@@ -108,6 +248,13 @@ declare const errorHandler: (err: any, ERROR_TYPE?: string, service?: string) =>
108
248
  success: boolean;
109
249
  service: string;
110
250
  };
251
+ /**
252
+ * Optional Express error-handling middleware built on top of `errorHandler`.
253
+ * Drop at the end of your middleware chain to catch all unhandled errors.
254
+ *
255
+ * @example
256
+ * app.use(expressErrorMiddleware());
257
+ */
111
258
  declare const expressErrorMiddleware: () => (err: any, req: any, res: any, next: any) => void;
112
259
 
113
260
  declare const paginate: (totalCount: number, currentPage: number, perPage: number) => {
@@ -117,55 +264,376 @@ declare const paginate: (totalCount: number, currentPage: number, perPage: numbe
117
264
  declare const formatDate: (date: Date) => string;
118
265
  declare const parseJSON: (value: any) => any;
119
266
  declare const stringifyJSON: (value: any) => any;
120
- declare const isObject: (val: any) => boolean;
121
- declare const sleep: (ms: number) => Promise<unknown>;
122
- declare const capitalize: (str: string) => string;
123
- declare const isEmpty: (val: any) => boolean;
124
267
 
268
+ type UUIDVersion = "v1" | "v4";
269
+ type UUIDBinary = Buffer;
270
+ type UUIDBuffer = Buffer;
125
271
  declare const uuid: {
126
- toBinary: (uuid?: any) => any;
127
- toString: (binary: any) => string;
128
- get: (version?: "v1" | "v4") => string;
129
- isValid: (uuid: string) => boolean;
130
- manyToString: (data: any, keys?: never[]) => any;
131
- manyToBinary: (data: any, keys?: never[]) => any;
272
+ /**
273
+ * Converts a UUID string to its optimized binary representation (Buffer).
274
+ * Reorders bytes for better index performance in databases like MySQL.
275
+ * If no UUID is provided, generates a new v1 UUID.
276
+ */
277
+ toBinary: (value?: string | UUIDBuffer) => UUIDBinary;
278
+ /**
279
+ * Converts a binary UUID Buffer back to its string representation.
280
+ */
281
+ toString: (binary: UUIDBinary | string) => string;
282
+ /**
283
+ * Generates a new UUID string.
284
+ * Defaults to v4 (random). Pass "v1" for time-based UUIDs.
285
+ *
286
+ * @example
287
+ * uuid.get() // v4 UUID
288
+ * uuid.get("v1") // v1 UUID
289
+ */
290
+ get: (version?: UUIDVersion) => string;
291
+ /**
292
+ * Returns true if the given string is a valid UUID.
293
+ */
294
+ isValid: (value: string) => boolean;
295
+ /** The nil UUID — all zeros. Useful as a default/placeholder. */
296
+ nil: "00000000-0000-0000-0000-000000000000";
297
+ /**
298
+ * Converts specified keys of an object from binary UUIDs to strings.
299
+ * Returns a shallow copy — does NOT mutate the original.
300
+ *
301
+ * @example
302
+ * uuid.manyToString({ id: <Buffer>, name: "foo" }, ["id"])
303
+ * // { id: "xxxxxxxx-...", name: "foo" }
304
+ */
305
+ manyToString: <T extends Record<string, any>>(data: T, keys?: string[]) => T;
306
+ /**
307
+ * Converts specified keys of an object from UUID strings to binary Buffers.
308
+ * Returns a shallow copy — does NOT mutate the original.
309
+ *
310
+ * @example
311
+ * uuid.manyToBinary({ id: "xxxxxxxx-...", name: "foo" }, ["id"])
312
+ * // { id: <Buffer>, name: "foo" }
313
+ */
314
+ manyToBinary: <T extends Record<string, any>>(data: T, keys?: string[]) => T;
132
315
  };
133
316
 
317
+ /**
318
+ * Pauses execution for the given number of milliseconds.
319
+ *
320
+ * @example
321
+ * await sleep(1000); // waits 1 second
322
+ */
323
+ declare const sleep: (ms: number) => Promise<void>;
324
+ interface RetryOptions {
325
+ /** Number of retry attempts (default: 3) */
326
+ retries?: number;
327
+ /** Base delay in ms (default: 500) */
328
+ delay?: number;
329
+ /** Use exponential backoff — doubles delay each attempt (default: true) */
330
+ exponential?: boolean;
331
+ /** Called on each failed attempt before retrying */
332
+ onError?: (err: unknown, attempt: number) => void;
333
+ }
334
+ /**
335
+ * Retries an async function on failure with optional exponential backoff.
336
+ *
337
+ * @example
338
+ * const data = await retry(() => fetchUser(id), { retries: 3, exponential: true });
339
+ */
340
+ declare const retry: <T>(fn: () => Promise<T>, options?: RetryOptions) => Promise<T>;
341
+ /**
342
+ * Rejects if the given promise doesn't resolve within `ms` milliseconds.
343
+ * Cleans up the internal timer whether the promise resolves or rejects.
344
+ *
345
+ * @example
346
+ * const data = await timeout(fetchUser(id), 5000);
347
+ */
348
+ declare const timeout: <T>(promise: Promise<T>, ms: number) => Promise<T>;
349
+ interface DebouncedFn<T extends (...args: any[]) => any> {
350
+ (...args: Parameters<T>): ReturnType<T> | undefined;
351
+ /** Cancels any pending invocation */
352
+ cancel: () => void;
353
+ /** Immediately invokes the pending call if one exists */
354
+ flush: (...args: Parameters<T>) => ReturnType<T> | undefined;
355
+ }
356
+ /**
357
+ * Returns a debounced version of `fn` that delays invocation until
358
+ * `delay`ms have passed since the last call.
359
+ *
360
+ * @example
361
+ * const onSearch = debounce((query: string) => search(query), 300);
362
+ * onSearch.cancel(); // cancel pending call
363
+ * onSearch.flush(); // invoke immediately
364
+ */
365
+ declare const debounce: <T extends (...args: any[]) => any>(fn: T, delay: number) => DebouncedFn<T>;
366
+ interface ThrottledFn<T extends (...args: any[]) => any> {
367
+ (...args: Parameters<T>): ReturnType<T> | undefined;
368
+ /** Cancels any pending trailing call */
369
+ cancel: () => void;
370
+ }
371
+ /**
372
+ * Returns a throttled version of `fn` that invokes at most once per `limit`ms.
373
+ * Executes on the leading edge and optionally on the trailing edge.
374
+ *
375
+ * @example
376
+ * const onScroll = throttle(() => updatePosition(), 100);
377
+ */
378
+ declare const throttle: <T extends (...args: any[]) => any>(fn: T, limit: number, { trailing }?: {
379
+ trailing?: boolean;
380
+ }) => ThrottledFn<T>;
381
+ /**
382
+ * Caches the result of `fn` based on its arguments.
383
+ * The cache key is built by JSON-serializing the arguments.
384
+ *
385
+ * @example
386
+ * const getUser = memoize((id: number) => fetchUser(id));
387
+ * await getUser(1); // fetches
388
+ * await getUser(1); // returns cached result
389
+ */
390
+ declare const memoize: <T extends (...args: any[]) => any>(fn: T, keyFn?: (...args: Parameters<T>) => string) => T & {
391
+ cache: Map<string, ReturnType<T>>;
392
+ clear: () => void;
393
+ };
394
+ /**
395
+ * Returns a version of `fn` that executes exactly once.
396
+ * All subsequent calls return the result of the first call.
397
+ *
398
+ * @example
399
+ * const init = once(() => setupDatabase());
400
+ * await init(); // runs
401
+ * await init(); // returns cached result, does not run again
402
+ */
403
+ declare const once: <T extends (...args: any[]) => any>(fn: T) => ((...args: Parameters<T>) => ReturnType<T>);
404
+
405
+ /**
406
+ * Flattens a nested object into a single-level object with dot-notation keys.
407
+ *
408
+ * @example
409
+ * flattenObject({ a: { b: { c: 1 } } }) // { "a.b.c": 1 }
410
+ * flattenObject({ a: { b: 1 } }, { separator: "_" }) // { "a_b": 1 }
411
+ */
412
+ declare const flattenObject: (obj: Record<string, any>, { separator, prefix }?: {
413
+ separator?: string;
414
+ prefix?: string;
415
+ }) => Record<string, any>;
416
+ /**
417
+ * Restores a flattened dot-notation object back to its nested form.
418
+ *
419
+ * @example
420
+ * unflattenObject({ "a.b.c": 1 }) // { a: { b: { c: 1 } } }
421
+ */
422
+ declare const unflattenObject: (obj: Record<string, any>, separator?: string) => Record<string, any>;
423
+
424
+ /**
425
+ * String utility functions
426
+ */
427
+ declare const splitWords: (str: string) => string[];
428
+ declare const capitalize: (str: string) => string;
429
+ declare const toUpperCase: (str: string) => string;
430
+ declare const toLowerCase: (str: string) => string;
431
+ declare const camelCase: (str: string) => string;
432
+ declare const pascalCase: (str: string) => string;
433
+ declare const snakeCase: (str: string) => string;
434
+ declare const kebabCase: (str: string) => string;
435
+ /**
436
+ * Truncates a string to `length` characters, appending `suffix` if trimmed.
437
+ * The suffix length is included in the total, so the result never exceeds `length`.
438
+ *
439
+ * @example
440
+ * truncate("Hello, world!", 8) // "Hello..."
441
+ * truncate("Hello, world!", 8, " →") // "Hello →"
442
+ */
443
+ declare const truncate: (str: string, length?: number, suffix?: string) => string;
444
+ /**
445
+ * Masks all but the last `visible` characters of a string.
446
+ * Useful for displaying sensitive values like credit cards or tokens.
447
+ *
448
+ * @example
449
+ * maskString("4111111111111234") // "************1234"
450
+ * maskString("mysecrettoken", 6) // "*******secret" ← last 6 visible
451
+ */
452
+ declare const maskString: (str: string, visible?: number) => string;
453
+ /**
454
+ * Returns true if the string contains only whitespace or is empty.
455
+ */
456
+ declare const isBlank: (str: string) => boolean;
457
+ /**
458
+ * Reverses a string.
459
+ */
460
+ declare const reverse: (str: string) => string;
461
+ /**
462
+ * Counts occurrences of `substr` within `str`.
463
+ */
464
+ declare const countOccurrences: (str: string, substr: string) => number;
465
+ /**
466
+ * Removes all extra whitespace — trims the string and collapses
467
+ * internal sequences of whitespace down to a single space.
468
+ *
469
+ * @example
470
+ * normalizeWhitespace(" hello world ") // "hello world"
471
+ */
472
+ declare const normalizeWhitespace: (str: string) => string;
473
+
474
+ /**
475
+ * Returns true for plain objects (not arrays, dates, null, etc.)
476
+ */
477
+ declare const isObject: (val: any) => boolean;
478
+ /**
479
+ * Validates an email address format.
480
+ */
481
+ declare const isEmail: (value: string) => boolean;
482
+ /**
483
+ * Validates a UUID v1–v5 string.
484
+ */
485
+ declare const isUUID: (value: string) => boolean;
486
+ /**
487
+ * Returns true for finite numbers (excludes NaN and Infinity).
488
+ */
489
+ declare const isNumber: (value: any) => boolean;
490
+ /**
491
+ * Returns true if the string is valid parseable JSON.
492
+ */
493
+ declare const isJSON: (value: string) => boolean;
494
+ /**
495
+ * Returns true for valid Date instances.
496
+ */
497
+ declare const isDate: (value: any) => boolean;
498
+ /**
499
+ * Returns true for valid http/https URLs only.
500
+ * Rejects data:, javascript:, and other URI schemes.
501
+ */
502
+ declare const isURL: (value: string) => boolean;
503
+ /**
504
+ * Returns true only for actual booleans (not truthy/falsy values).
505
+ */
506
+ declare const isBoolean: (value: any) => boolean;
507
+ /**
508
+ * Returns true for strings.
509
+ */
510
+ declare const isString: (value: any) => boolean;
511
+ /**
512
+ * Returns true for arrays.
513
+ */
514
+ declare const isArray: (value: any) => boolean;
515
+ /**
516
+ * Returns true for integers (excludes floats, NaN, Infinity).
517
+ */
518
+ declare const isInteger: (value: any) => boolean;
519
+ /**
520
+ * Returns true for positive numbers (excludes zero).
521
+ */
522
+ declare const isPositive: (value: any) => boolean;
523
+ /**
524
+ * Returns true for negative numbers (excludes zero).
525
+ */
526
+ declare const isNegative: (value: any) => boolean;
527
+ /**
528
+ * Returns true if value is null or undefined.
529
+ */
530
+ declare const isNil: (value: any) => boolean;
531
+ /**
532
+ * Returns true if the value is "empty":
533
+ * - null / undefined
534
+ * - empty string or whitespace-only string
535
+ * - empty array
536
+ * - empty plain object
537
+ */
538
+ declare const isEmpty: (val: any) => boolean;
539
+
540
+ interface Logger$1 {
541
+ info: (msg: string, meta?: any) => void;
542
+ error: (msg: string, meta?: any) => void;
543
+ warn: (msg: string, meta?: any) => void;
544
+ debug: (msg: string, meta?: any) => void;
545
+ }
134
546
  declare class Redis {
135
- client: RedisClient;
136
- constructor(url: string, options?: RedisOptions);
547
+ private client;
548
+ private logger;
549
+ constructor(url: string, options?: RedisOptions, logger?: Logger$1);
137
550
  private registerListeners;
138
551
  start(): Promise<void>;
139
552
  disconnect(): Promise<void>;
140
- keys(pattern: string): Promise<string[]>;
553
+ private validateKey;
554
+ private buildKey;
141
555
  private serialize;
142
556
  private deserialize;
143
557
  set(key: string, data: any): Promise<"OK">;
144
558
  setEx(key: string, data: any, duration: number | string): Promise<"OK">;
145
559
  get<T = any>(key: string, parse?: boolean): Promise<T | null>;
146
560
  delete(key: string): Promise<boolean>;
147
- deleteAll(prefix: string): Promise<number>;
148
561
  exists(key: string): Promise<boolean>;
149
562
  ttl(key: string): Promise<number>;
150
563
  expire(key: string, duration: number | string): Promise<boolean>;
151
- flush(): Promise<void>;
564
+ /**
565
+ * Atomically increments a counter. Creates it at 1 if it doesn't exist.
566
+ * Optionally sets a TTL on first creation.
567
+ *
568
+ * @example
569
+ * await redis.increment("rate:user:123"); // 1, 2, 3...
570
+ * await redis.increment("rate:user:123", "1 hour"); // resets TTL each time
571
+ */
572
+ increment(key: string, ttl?: number | string): Promise<number>;
573
+ /**
574
+ * Atomically decrements a counter.
575
+ */
576
+ decrement(key: string): Promise<number>;
577
+ /**
578
+ * Sets one or more fields on a Redis hash.
579
+ *
580
+ * @example
581
+ * await redis.hset("user:1", { name: "Alice", role: "admin" });
582
+ */
583
+ hset(key: string, data: Record<string, any>): Promise<number>;
584
+ /**
585
+ * Gets a single field from a Redis hash.
586
+ */
587
+ hget<T = any>(key: string, field: string): Promise<T | null>;
588
+ /**
589
+ * Gets all fields from a Redis hash as a typed object.
590
+ */
591
+ hgetAll<T extends Record<string, any> = Record<string, any>>(key: string): Promise<T | null>;
592
+ /**
593
+ * Deletes one or more fields from a Redis hash.
594
+ */
595
+ hdel(key: string, ...fields: string[]): Promise<number>;
596
+ /**
597
+ * Safely scans for keys matching a pattern using SCAN (non-blocking).
598
+ * Prefer this over KEYS in production — KEYS blocks the event loop.
599
+ *
600
+ * @example
601
+ * await redis.scan("user:*") // ["user:1", "user:2", ...]
602
+ */
603
+ scan(pattern: string): Promise<string[]>;
604
+ /**
605
+ * Deletes all keys matching a pattern using SCAN + batched DEL.
606
+ * Safe for large keyspaces.
607
+ *
608
+ * @example
609
+ * await redis.deleteByPattern("session:*") // clears all sessions
610
+ */
611
+ deleteByPattern(pattern: string): Promise<number>;
612
+ /**
613
+ * @deprecated Use `scan()` instead — KEYS blocks the Redis event loop.
614
+ */
615
+ keys(pattern: string): Promise<string[]>;
616
+ /**
617
+ * @deprecated Use `deleteByPattern()` instead.
618
+ */
619
+ deleteAll(prefix: string): Promise<number>;
620
+ /**
621
+ * Flushes the current database. Intended for testing only.
622
+ * Throws in production unless `force: true` is passed.
623
+ */
624
+ flush(force?: boolean): Promise<void>;
625
+ private authKey;
626
+ private tokenKey;
152
627
  getCachedUser<T = any>(id: string, throwError?: boolean): Promise<T | null>;
153
628
  cacheUser(user: any, ttl?: number | string): Promise<void>;
629
+ /**
630
+ * Atomically updates an array field on a cached user.
631
+ * Operates on a fresh copy to avoid mutating the cached object before re-save.
632
+ */
154
633
  updateAuthData(userId: string, key: string, value: string, action?: "ADD" | "REMOVE"): Promise<any>;
155
634
  private parseDuration;
156
635
  }
157
636
 
158
- interface SQSDequeueInt {
159
- queueUrl: string;
160
- consumerFunction: (message: any) => Promise<any>;
161
- maxNumberOfMessages?: number;
162
- waitTimeSeconds?: number;
163
- dlqUrl?: string;
164
- }
165
- interface SQSEqueueInt {
166
- queueUrl: string;
167
- message: any;
168
- }
169
637
  interface Logger {
170
638
  info(message: string, meta?: unknown): void;
171
639
  error(message: string, meta?: unknown): void;
@@ -178,20 +646,314 @@ interface SqsConfig {
178
646
  accessKeyId: string;
179
647
  secretAccessKey: string;
180
648
  }
649
+ interface SQSEnqueueOptions {
650
+ queueUrl: string;
651
+ message: string | object;
652
+ /** Required for FIFO queues */
653
+ messageGroupId?: string;
654
+ /** Required for FIFO queues with content-based deduplication disabled */
655
+ messageDeduplicationId?: string;
656
+ /** Delay message delivery (0–900 seconds) */
657
+ delaySeconds?: number;
658
+ /** Arbitrary metadata attached to the message */
659
+ attributes?: Record<string, MessageAttributeValue>;
660
+ }
661
+ interface SQSDequeueOptions {
662
+ queueUrl: string;
663
+ consumerFunction: (message: any) => Promise<void>;
664
+ dlqUrl?: string;
665
+ maxNumberOfMessages?: number;
666
+ waitTimeSeconds?: number;
667
+ /**
668
+ * Extend visibility timeout during processing (seconds).
669
+ * Set this close to your expected max processing time.
670
+ */
671
+ visibilityTimeout?: number;
672
+ /**
673
+ * If true, failed messages are left in the queue for SQS to retry
674
+ * via the queue's own redrive policy instead of immediately going to DLQ.
675
+ */
676
+ useRedrivePolicy?: boolean;
677
+ }
181
678
  declare class SQS {
182
679
  private client;
183
680
  private logger;
681
+ private polling;
184
682
  constructor(config: SqsConfig, logger?: Logger);
185
- enqueue({ queueUrl, message }: SQSEqueueInt): Promise<boolean>;
186
- dequeue(fields: SQSDequeueInt): Promise<void>;
683
+ /**
684
+ * Sends a message to an SQS queue.
685
+ * Automatically serializes objects to JSON.
686
+ *
687
+ * @example
688
+ * await sqs.enqueue({ queueUrl, message: { event: "user.created", userId: 1 } });
689
+ */
690
+ enqueue({ queueUrl, message, messageGroupId, messageDeduplicationId, delaySeconds, attributes, }: SQSEnqueueOptions): Promise<boolean>;
691
+ /**
692
+ * Starts long-polling a queue and passes each message to `consumerFunction`.
693
+ * Runs until `stop()` is called.
694
+ *
695
+ * Delete behaviour:
696
+ * - On success → always deletes
697
+ * - On failure + DLQ → moves to DLQ, then deletes
698
+ * - On failure + useRedrivePolicy → does NOT delete (lets SQS retry)
699
+ * - On failure + no DLQ + no redrive → logs and deletes to avoid poison pill loop
700
+ */
701
+ dequeue({ queueUrl, consumerFunction, dlqUrl, maxNumberOfMessages, waitTimeSeconds, visibilityTimeout, useRedrivePolicy, }: SQSDequeueOptions): Promise<void>;
702
+ /**
703
+ * Gracefully stops the polling loop after the current batch completes.
704
+ */
705
+ stop(): void;
706
+ private processMessage;
187
707
  }
188
708
 
709
+ type LogLevel = "error" | "warn" | "info" | "http" | "debug";
710
+ interface WinstonLoggerOptions {
711
+ /** Minimum log level to output (default: "info", or "debug" in development) */
712
+ level?: LogLevel;
713
+ /** Service name attached to every log entry */
714
+ service?: string;
715
+ /** If true, write logs to a file in addition to the console */
716
+ file?: {
717
+ path: string;
718
+ /** Separate file for errors only (recommended) */
719
+ errorPath?: string;
720
+ };
721
+ /** If true, output plain text instead of JSON (default: true in development) */
722
+ pretty?: boolean;
723
+ /** Static metadata attached to every log entry */
724
+ defaultMeta?: Record<string, unknown>;
725
+ }
189
726
  declare class WinstonLogger implements Logger {
190
727
  private logger;
728
+ constructor(options?: WinstonLoggerOptions);
191
729
  info(message: string, meta?: unknown): void;
192
730
  error(message: string, meta?: unknown): void;
193
731
  warn(message: string, meta?: unknown): void;
194
732
  debug(message: string, meta?: unknown): void;
733
+ http(message: string, meta?: unknown): void;
734
+ /**
735
+ * Returns a child logger with additional metadata attached to every entry.
736
+ * Useful for scoping logs to a request, service, or job.
737
+ *
738
+ * @example
739
+ * const log = logger.child({ requestId: "abc-123", userId: "u-1" });
740
+ * log.info("User fetched"); // → { requestId: "abc-123", userId: "u-1", message: "User fetched" }
741
+ */
742
+ child(meta: Record<string, unknown>): WinstonLogger;
743
+ /**
744
+ * Dynamically changes the log level at runtime.
745
+ * Useful for temporarily enabling debug logs in production.
746
+ *
747
+ * @example
748
+ * logger.setLevel("debug");
749
+ */
750
+ setLevel(level: LogLevel): void;
751
+ /**
752
+ * Returns true if the given level would currently be logged.
753
+ *
754
+ * @example
755
+ * if (logger.isLevelEnabled("debug")) { ... }
756
+ */
757
+ isLevelEnabled(level: LogLevel): boolean;
758
+ }
759
+
760
+ interface S3Config {
761
+ region: string;
762
+ accessKeyId: string;
763
+ secretAccessKey: string;
764
+ defaultBucket?: string;
765
+ }
766
+ interface S3UploadOptions {
767
+ bucket?: string;
768
+ key: string;
769
+ body: Buffer | Uint8Array | string | Readable;
770
+ contentType?: string;
771
+ metadata?: Record<string, string>;
772
+ /** Canned ACL e.g. "private" | "public-read" */
773
+ acl?: ObjectCannedACL;
774
+ }
775
+ interface S3UploadResult {
776
+ bucket: string;
777
+ key: string;
778
+ url: string;
779
+ }
780
+ interface S3ObjectOptions {
781
+ bucket?: string;
782
+ key: string;
783
+ }
784
+ interface S3CopyOptions {
785
+ sourceBucket?: string;
786
+ sourceKey: string;
787
+ destinationBucket?: string;
788
+ destinationKey: string;
789
+ }
790
+ interface S3SignedUrlOptions {
791
+ bucket?: string;
792
+ key: string;
793
+ expiresIn?: number;
794
+ }
795
+ declare class S3 {
796
+ private client;
797
+ private logger;
798
+ private defaultBucket?;
799
+ private region;
800
+ constructor(config: S3Config, logger?: Logger);
801
+ private getBucket;
802
+ private getObjectUrl;
803
+ private streamToBuffer;
804
+ /**
805
+ * Uploads a file to S3. Returns the bucket, key, and public URL.
806
+ *
807
+ * @example
808
+ * const result = await s3.upload({ key: "avatars/user-1.png", body: buffer, contentType: "image/png" });
809
+ * result.url // "https://my-bucket.s3.us-east-1.amazonaws.com/avatars/user-1.png"
810
+ */
811
+ upload({ bucket, key, body, contentType, metadata, acl, }: S3UploadOptions): Promise<S3UploadResult>;
812
+ /**
813
+ * Downloads an S3 object and returns it as a Buffer.
814
+ */
815
+ download({ bucket, key }: S3ObjectOptions): Promise<Buffer>;
816
+ /**
817
+ * Returns the raw readable stream for an S3 object.
818
+ * Prefer this over `download` for large files.
819
+ */
820
+ stream({ bucket, key }: S3ObjectOptions): Promise<Readable>;
821
+ delete({ bucket, key }: S3ObjectOptions): Promise<boolean>;
822
+ /**
823
+ * Copies an object within S3 — within the same bucket or across buckets.
824
+ *
825
+ * @example
826
+ * await s3.copy({ sourceKey: "uploads/tmp.png", destinationKey: "avatars/user-1.png" });
827
+ */
828
+ copy({ sourceBucket, sourceKey, destinationBucket, destinationKey, }: S3CopyOptions): Promise<S3UploadResult>;
829
+ /**
830
+ * Returns true if the object exists.
831
+ * Throws on non-404 errors (permissions, network) rather than silently returning false.
832
+ */
833
+ exists({ bucket, key }: S3ObjectOptions): Promise<boolean>;
834
+ /**
835
+ * Generates a pre-signed URL for downloading an object (GET).
836
+ * Default expiry: 1 hour.
837
+ */
838
+ getSignedDownloadUrl({ bucket, key, expiresIn }: S3SignedUrlOptions): Promise<string>;
839
+ /**
840
+ * Generates a pre-signed URL for uploading an object directly (PUT).
841
+ * Use this for browser → S3 direct uploads without proxying through your server.
842
+ *
843
+ * @example
844
+ * const url = await s3.getSignedUploadUrl({ key: "avatars/user-1.png", contentType: "image/png" });
845
+ * // Client does: fetch(url, { method: "PUT", body: file })
846
+ */
847
+ getSignedUploadUrl({ bucket, key, expiresIn, contentType, }: S3SignedUrlOptions & {
848
+ contentType?: string;
849
+ }): Promise<string>;
850
+ /**
851
+ * Returns a scoped helper with the bucket pre-filled.
852
+ *
853
+ * @example
854
+ * const avatars = s3.bucket("my-avatars-bucket");
855
+ * await avatars.upload({ key: "user-1.png", body: buffer });
856
+ */
857
+ bucket(bucketName: string): {
858
+ upload: (opts: Omit<S3UploadOptions, "bucket">) => Promise<S3UploadResult>;
859
+ download: (opts: Omit<S3ObjectOptions, "bucket">) => Promise<Buffer<ArrayBufferLike>>;
860
+ stream: (opts: Omit<S3ObjectOptions, "bucket">) => Promise<Readable>;
861
+ delete: (opts: Omit<S3ObjectOptions, "bucket">) => Promise<boolean>;
862
+ exists: (opts: Omit<S3ObjectOptions, "bucket">) => Promise<boolean>;
863
+ copy: (opts: Omit<S3CopyOptions, "destinationBucket">) => Promise<S3UploadResult>;
864
+ getSignedDownloadUrl: (opts: Omit<S3SignedUrlOptions, "bucket">) => Promise<string>;
865
+ getSignedUploadUrl: (opts: Omit<S3SignedUrlOptions & {
866
+ contentType?: string;
867
+ }, "bucket">) => Promise<string>;
868
+ };
869
+ }
870
+
871
+ interface CronJobOptions {
872
+ /** Cron expression e.g. "0 * * * *" or human shorthand */
873
+ schedule: string;
874
+ /** Human-readable name for logging and lookup */
875
+ name: string;
876
+ /** The function to execute on each tick */
877
+ handler: () => Promise<void> | void;
878
+ /** If true, runs the handler immediately on registration (default: false) */
879
+ runOnInit?: boolean;
880
+ /** Timezone e.g. "America/New_York" (default: system timezone) */
881
+ timezone?: string;
882
+ /** If true, prevents overlapping executions — waits for current run to finish (default: true) */
883
+ preventOverlap?: boolean;
884
+ }
885
+ interface CronJobStatus {
886
+ name: string;
887
+ schedule: string;
888
+ running: boolean;
889
+ lastRun: Date | null;
890
+ lastError: Error | null;
891
+ executionCount: number;
892
+ errorCount: number;
893
+ }
894
+ declare class Cron {
895
+ private jobs;
896
+ private logger;
897
+ constructor(logger?: Logger);
898
+ /**
899
+ * Registers and starts a cron job.
900
+ *
901
+ * @example
902
+ * cron.register({
903
+ * name: "send-digest",
904
+ * schedule: "every day at noon",
905
+ * handler: async () => { await sendDigestEmails(); },
906
+ * timezone: "America/New_York",
907
+ * });
908
+ */
909
+ register(options: CronJobOptions): void;
910
+ private execute;
911
+ /**
912
+ * Stops a running job without removing it.
913
+ * Can be resumed with start().
914
+ */
915
+ stop(name: string): void;
916
+ /**
917
+ * Resumes a stopped job.
918
+ */
919
+ start(name: string): void;
920
+ /**
921
+ * Stops and removes a job entirely.
922
+ */
923
+ remove(name: string): void;
924
+ /**
925
+ * Replaces an existing job with a new configuration.
926
+ * Useful for updating schedules at runtime.
927
+ */
928
+ replace(options: CronJobOptions): void;
929
+ /**
930
+ * Manually triggers a job outside its schedule.
931
+ * Respects preventOverlap.
932
+ *
933
+ * @example
934
+ * await cron.run("send-digest");
935
+ */
936
+ run(name: string): Promise<void>;
937
+ /**
938
+ * Stops all registered jobs. Call this on process shutdown.
939
+ *
940
+ * @example
941
+ * process.on("SIGTERM", () => cron.stopAll());
942
+ */
943
+ stopAll(): void;
944
+ /**
945
+ * Returns the status of a single job.
946
+ */
947
+ status(name: string): CronJobStatus;
948
+ /**
949
+ * Returns the status of all registered jobs.
950
+ */
951
+ statusAll(): CronJobStatus[];
952
+ /**
953
+ * Returns true if a job with the given name is registered.
954
+ */
955
+ has(name: string): boolean;
956
+ private getJob;
195
957
  }
196
958
 
197
959
  interface JwtEncodeOptions {
@@ -206,8 +968,129 @@ interface JwtDecodeOptions {
206
968
  algorithms?: string[];
207
969
  }
208
970
  declare const jwtService: {
971
+ /**
972
+ * Signs a payload and returns a JWT string.
973
+ *
974
+ * @example
975
+ * const token = await jwtService.encode({ data: { userId: 1 }, secretKey: "secret" });
976
+ */
209
977
  encode({ data, secretKey, expiresIn, algorithm, }: JwtEncodeOptions): Promise<string>;
978
+ /**
979
+ * Verifies and decodes a JWT string.
980
+ * Throws a typed `JwtError` on expiry, invalid signature, or not-yet-valid tokens.
981
+ *
982
+ * @example
983
+ * const payload = await jwtService.decode<{ userId: number }>({ token, secretKey: "secret" });
984
+ */
210
985
  decode<T = jwt.JwtPayload>({ token, secretKey, algorithms, }: JwtDecodeOptions): Promise<T>;
986
+ /**
987
+ * Returns the expiry date of a token without verifying it.
988
+ * Returns null if the token has no expiry or cannot be decoded.
989
+ *
990
+ * @example
991
+ * jwtService.getExpiry(token) // Date | null
992
+ */
993
+ getExpiry(token: string): Date | null;
994
+ /**
995
+ * Returns true if the token is expired, without verifying the signature.
996
+ * Useful for checking whether to refresh a token before making a request.
997
+ *
998
+ * @example
999
+ * if (jwtService.isExpired(token)) { ... }
1000
+ */
1001
+ isExpired(token: string): boolean;
1002
+ };
1003
+
1004
+ interface HashOptions {
1005
+ /** bcrypt salt rounds (default: 12) */
1006
+ rounds?: number;
1007
+ }
1008
+ interface HmacOptions {
1009
+ /** HMAC algorithm (default: "sha256") */
1010
+ algorithm?: "sha256" | "sha512" | "sha1";
1011
+ /** Output encoding (default: "hex") */
1012
+ encoding?: "hex" | "base64";
1013
+ }
1014
+ interface TokenOptions {
1015
+ /** Byte length of the random token (default: 32) */
1016
+ bytes?: number;
1017
+ /** Output encoding (default: "hex") */
1018
+ encoding?: "hex" | "base64url" | "base64";
1019
+ }
1020
+ declare const hashService: {
1021
+ /**
1022
+ * Hashes a plain text value using bcrypt.
1023
+ * Use for passwords — bcrypt is intentionally slow and salted.
1024
+ *
1025
+ * @example
1026
+ * const hashed = await hashService.hash("myPassword123");
1027
+ */
1028
+ hash(plain: string, { rounds }?: HashOptions): Promise<string>;
1029
+ /**
1030
+ * Compares a plain text value against a bcrypt hash.
1031
+ *
1032
+ * @example
1033
+ * const match = await hashService.compare("myPassword123", storedHash);
1034
+ * if (!match) throw new AuthenticationError("Invalid credentials");
1035
+ */
1036
+ compare(plain: string, hashed: string): Promise<boolean>;
1037
+ /**
1038
+ * Returns true if the string looks like a bcrypt hash.
1039
+ * Useful for detecting already-hashed values before double-hashing.
1040
+ *
1041
+ * @example
1042
+ * hashService.isBcryptHash("$2b$12$...") // true
1043
+ */
1044
+ isBcryptHash(value: string): boolean;
1045
+ /**
1046
+ * Creates an HMAC signature for a value using a secret key.
1047
+ * Use for signing data (webhooks, tokens, URLs) — NOT for passwords.
1048
+ *
1049
+ * @example
1050
+ * const sig = hashService.hmac("payload body", process.env.WEBHOOK_SECRET);
1051
+ */
1052
+ hmac(value: string, secret: string, { algorithm, encoding }?: HmacOptions): string;
1053
+ /**
1054
+ * Verifies an HMAC signature using a timing-safe comparison.
1055
+ * Always use this instead of `===` to prevent timing attacks.
1056
+ *
1057
+ * @example
1058
+ * const valid = hashService.verifyHmac(payload, secret, incomingSignature);
1059
+ * if (!valid) throw new Error("Invalid webhook signature");
1060
+ */
1061
+ verifyHmac(value: string, secret: string, signature: string, options?: HmacOptions): boolean;
1062
+ /**
1063
+ * Creates a one-way SHA hash of a value (no secret).
1064
+ * Use for content fingerprinting, cache keys, or deduplication.
1065
+ * NOT suitable for passwords.
1066
+ *
1067
+ * @example
1068
+ * const fingerprint = hashService.sha256("file contents here");
1069
+ */
1070
+ sha256(value: string, encoding?: "hex" | "base64"): string;
1071
+ sha512(value: string, encoding?: "hex" | "base64"): string;
1072
+ /**
1073
+ * Generates a cryptographically secure random token.
1074
+ * Use for password reset tokens, email verification, API keys, etc.
1075
+ *
1076
+ * @example
1077
+ * const token = hashService.generateToken(); // 64-char hex string
1078
+ * const token = hashService.generateToken({ bytes: 16, encoding: "base64url" });
1079
+ */
1080
+ generateToken({ bytes, encoding }?: TokenOptions): string;
1081
+ /**
1082
+ * Generates a token and returns both the raw value (to send to user)
1083
+ * and its SHA-256 hash (to store in the database).
1084
+ *
1085
+ * @example
1086
+ * const { token, hashed } = hashService.generateHashedToken();
1087
+ * await db.user.update({ resetToken: hashed, resetTokenExpiry: ... });
1088
+ * await email.send({ to: user.email, token }); // send raw token to user
1089
+ */
1090
+ generateHashedToken(options?: TokenOptions): {
1091
+ token: string;
1092
+ hashed: string;
1093
+ };
211
1094
  };
212
1095
 
213
- export { AppError, AuthenticationError, AuthorizationError, BadRequestError, ExistingError, HTTP_STATUS, HTTP_STATUS_CODE_ERROR, HttpStatus, HttpStatusKey, JwtDecodeOptions, JwtEncodeOptions, NoContent, NotFoundError, Redis, SQS, ServerError, SqsConfig, TokenExpiredError, TokenInvalidError, ValidationError, WinstonLogger, capitalize, errorHandler, expressErrorMiddleware, formatDate, isEmpty, isObject, joiValidator, jwtService, makeRequest, paginate, parseJSON, sleep, stringifyJSON, uuid };
1096
+ export { AppError, AuthenticationError, AuthorizationError, BadRequestError, Cron, CronJobOptions, CronJobStatus, DebouncedFn, DirectConstraint, ExistingError, FieldConstraint, HTTP_STATUS, HTTP_STATUS_CODE_ERROR, HashOptions, HmacOptions, HttpStatus, HttpStatusKey, JoiConstraints, JwtDecodeOptions, JwtEncodeOptions, LogLevel, NoContent, NotFoundError, Redis, RetryOptions, S3, S3Config, S3CopyOptions, S3ObjectOptions, S3SignedUrlOptions, S3UploadOptions, S3UploadResult, SQS, SQSDequeueOptions, SQSEnqueueOptions, ServerError, SqsConfig, ThrottledFn, TokenExpiredError, TokenInvalidError, TokenOptions, ValidationError, WinstonLogger, WinstonLoggerOptions, camelCase, capitalize, countOccurrences, debounce, errorHandler, expressErrorMiddleware, flattenObject, formatDate, hashService, isArray, isBlank, isBoolean, isDate, isEmail, isEmpty, isInteger, isJSON, isNegative, isNil, isNumber, isObject, isPositive, isString, isURL, isUUID, joiMiddleware, joiValidate, jwtService, kebabCase, makeRequest, maskString, memoize, normalizeWhitespace, once, paginate, parseJSON, pascalCase, retry, reverse, sleep, snakeCase, splitWords, stringifyJSON, throttle, timeout, toLowerCase, toUpperCase, truncate, unflattenObject, uuid };