@riocrypto/common-server 1.0.2275 → 1.0.2277

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.
@@ -47,6 +47,7 @@
47
47
  /// <reference types="mongoose/types/virtuals" />
48
48
  /// <reference types="mongoose" />
49
49
  /// <reference types="mongoose/types/inferschematype" />
50
+ /// <reference types="mongoose/types/inferrawdoctype" />
50
51
  /// <reference types="mongoose/types/inferschematype" />
51
52
  /// <reference types="mongoose/types/inferrawdoctype" />
52
53
  import { BankAccountDoc } from "@riocrypto/common-server";
@@ -47,6 +47,7 @@
47
47
  /// <reference types="mongoose/types/virtuals" />
48
48
  /// <reference types="mongoose" />
49
49
  /// <reference types="mongoose/types/inferschematype" />
50
+ /// <reference types="mongoose/types/inferrawdoctype" />
50
51
  /// <reference types="mongoose/types/inferschematype" />
51
52
  /// <reference types="mongoose/types/inferrawdoctype" />
52
53
  import { CryptoAddressDoc } from "@riocrypto/common-server";
@@ -47,6 +47,7 @@
47
47
  /// <reference types="mongoose/types/virtuals" />
48
48
  /// <reference types="mongoose" />
49
49
  /// <reference types="mongoose/types/inferschematype" />
50
+ /// <reference types="mongoose/types/inferrawdoctype" />
50
51
  /// <reference types="mongoose/types/inferschematype" />
51
52
  /// <reference types="mongoose/types/inferrawdoctype" />
52
53
  import { OrderDoc } from "@riocrypto/common-server";
@@ -47,6 +47,7 @@
47
47
  /// <reference types="mongoose/types/virtuals" />
48
48
  /// <reference types="mongoose" />
49
49
  /// <reference types="mongoose/types/inferschematype" />
50
+ /// <reference types="mongoose/types/inferrawdoctype" />
50
51
  /// <reference types="mongoose/types/inferschematype" />
51
52
  /// <reference types="mongoose/types/inferrawdoctype" />
52
53
  import { UserDoc } from "@riocrypto/common-server";
package/build/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./middlewares/set-trace";
7
7
  export * from "./middlewares/require-min-role";
8
8
  export * from "./middlewares/require-min-admin-role";
9
9
  export * from "./middlewares/verify-csrf-token";
10
+ export * from "./middlewares/dynamic-rate-limiter";
10
11
  export * from "./services/apiKey";
11
12
  export * from "./services/password";
12
13
  export * from "./services/logger";
@@ -91,6 +92,7 @@ export * from "./models/STP-settings";
91
92
  export * from "./models/dashboard-push-notification-subscription";
92
93
  export * from "./models/webauthn-credential";
93
94
  export * from "./models/admin-webauthn-credential";
95
+ export * from "./models/custom-rate-limit";
94
96
  export * from "./clients/axios-with-logging";
95
97
  export * from "./clients/slack-client";
96
98
  export * from "./clients/fireblocks-client";
package/build/index.js CHANGED
@@ -23,6 +23,7 @@ __exportStar(require("./middlewares/set-trace"), exports);
23
23
  __exportStar(require("./middlewares/require-min-role"), exports);
24
24
  __exportStar(require("./middlewares/require-min-admin-role"), exports);
25
25
  __exportStar(require("./middlewares/verify-csrf-token"), exports);
26
+ __exportStar(require("./middlewares/dynamic-rate-limiter"), exports);
26
27
  __exportStar(require("./services/apiKey"), exports);
27
28
  __exportStar(require("./services/password"), exports);
28
29
  __exportStar(require("./services/logger"), exports);
@@ -107,6 +108,7 @@ __exportStar(require("./models/STP-settings"), exports);
107
108
  __exportStar(require("./models/dashboard-push-notification-subscription"), exports);
108
109
  __exportStar(require("./models/webauthn-credential"), exports);
109
110
  __exportStar(require("./models/admin-webauthn-credential"), exports);
111
+ __exportStar(require("./models/custom-rate-limit"), exports);
110
112
  __exportStar(require("./clients/axios-with-logging"), exports);
111
113
  __exportStar(require("./clients/slack-client"), exports);
112
114
  __exportStar(require("./clients/fireblocks-client"), exports);
@@ -0,0 +1,10 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { Options as RateLimitOptions } from "express-rate-limit";
3
+ import { Mongoose } from "mongoose";
4
+ export interface DynamicRateLimiterOptions extends Partial<RateLimitOptions> {
5
+ windowMs: number;
6
+ max: number;
7
+ mongooseConnection: Mongoose;
8
+ cacheTtlMs?: number;
9
+ }
10
+ export declare function createDynamicRateLimiter(options: DynamicRateLimiterOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createDynamicRateLimiter = void 0;
27
+ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
28
+ // Removed path-to-regexp import
29
+ // Adjust the import path according to your project structure
30
+ const custom_rate_limit_1 = require("../models/custom-rate-limit");
31
+ // Cache for rate limiter instances
32
+ const limiterInstanceCache = new Map();
33
+ // Cache for compiled RegExp objects from path strings
34
+ // Key: regex string pattern, Value: compiled RegExp object
35
+ const regexCache = new Map();
36
+ // In-memory cache for custom IP rules
37
+ let customIpRulesCache = new Map();
38
+ let lastCacheRefreshTime = 0;
39
+ const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
40
+ function refreshCustomIpRulesCache(CustomRateLimit, filePathForLog) {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ try {
43
+ const allRules = yield CustomRateLimit.find({}).lean();
44
+ const newCache = new Map();
45
+ allRules.forEach((rule) => {
46
+ if (rule.ipAddress) {
47
+ // Pre-compile and cache regexes when rules are loaded/refreshed
48
+ if (rule.routes && Array.isArray(rule.routes)) {
49
+ rule.routes.forEach((routeRule) => {
50
+ // Use any temporarily if lean typing is tricky
51
+ if (typeof routeRule.path === "string" &&
52
+ !regexCache.has(routeRule.path)) {
53
+ try {
54
+ regexCache.set(routeRule.path, new RegExp(routeRule.path));
55
+ }
56
+ catch (e) {
57
+ console.error(`[DynamicRateLimiter Refresh] Failed to compile regex: '${routeRule.path}' for IP ${rule.ipAddress}. Skipping rule path. Error:`, e);
58
+ // Optionally mark the rule path as invalid or handle differently
59
+ }
60
+ }
61
+ });
62
+ }
63
+ newCache.set(rule.ipAddress, rule);
64
+ }
65
+ });
66
+ customIpRulesCache = newCache;
67
+ lastCacheRefreshTime = Date.now();
68
+ }
69
+ catch (error) {
70
+ console.error(`[DynamicRateLimiter ${filePathForLog}] Error refreshing custom IP rules cache:`, error);
71
+ }
72
+ });
73
+ }
74
+ function createDynamicRateLimiter(options) {
75
+ const { windowMs: defaultWindowMs, // Rename for clarity
76
+ max: defaultMax, // Rename for clarity
77
+ mongooseConnection, cacheTtlMs = DEFAULT_CACHE_TTL_MS,
78
+ // Capture other express-rate-limit options passed in
79
+ message, standardHeaders, legacyHeaders, skip, requestPropertyName } = options,
80
+ // Add any other valid RateLimitOptions you might pass
81
+ otherRateLimitOptions = __rest(options, ["windowMs", "max", "mongooseConnection", "cacheTtlMs", "message", "standardHeaders", "legacyHeaders", "skip", "requestPropertyName"]) // Use a specific variable if you know exactly what else you pass
82
+ ;
83
+ if (!mongooseConnection) {
84
+ throw new Error("[DynamicRateLimiter] Mongoose connection is required.");
85
+ }
86
+ const CustomRateLimit = (0, custom_rate_limit_1.buildCustomRateLimit)(mongooseConnection);
87
+ refreshCustomIpRulesCache(CustomRateLimit, "initialization");
88
+ return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
89
+ try {
90
+ // Refresh cache if stale
91
+ if (Date.now() - lastCacheRefreshTime > cacheTtlMs ||
92
+ (customIpRulesCache.size === 0 && lastCacheRefreshTime === 0)) {
93
+ yield refreshCustomIpRulesCache(CustomRateLimit, req.path);
94
+ }
95
+ const clientIp = req.ip;
96
+ let currentLimit = defaultMax; // Use destructured default
97
+ let currentWindowMs = defaultWindowMs; // Use destructured default
98
+ let ruleIdentifier = "default";
99
+ let customRule;
100
+ if (clientIp) {
101
+ customRule = customIpRulesCache.get(clientIp);
102
+ }
103
+ if (customRule) {
104
+ // Check if routes array exists and is not empty
105
+ const appliesToCurrentRoute = !customRule.routes || // Should not happen if default is []
106
+ customRule.routes.length === 0 || // Empty array means apply custom limit to ALL requests for this IP
107
+ customRule.routes.some((rule) => {
108
+ // Check HTTP Method first (faster check)
109
+ const methodMatch = rule.action === "*" || rule.action === req.method.toUpperCase();
110
+ if (!methodMatch) {
111
+ return false;
112
+ }
113
+ // Get the compiled regex from cache
114
+ const regex = regexCache.get(rule.path);
115
+ if (!regex) {
116
+ // Regex failed compilation during refresh, skip this rule path
117
+ // Error was already logged during refreshCustomIpRulesCache
118
+ return false;
119
+ }
120
+ // Test the path against the regex
121
+ return regex.test(req.path);
122
+ });
123
+ if (appliesToCurrentRoute) {
124
+ currentLimit = customRule.limit;
125
+ currentWindowMs = customRule.windowMs;
126
+ ruleIdentifier = `custom_ip:${clientIp}`;
127
+ }
128
+ }
129
+ // Get/create limiter instance using currentLimit, currentWindowMs, ruleIdentifier
130
+ const limiterCacheKey = `${currentLimit}-${currentWindowMs}-${ruleIdentifier}`;
131
+ let selectedLimiter = limiterInstanceCache.get(limiterCacheKey);
132
+ if (!selectedLimiter) {
133
+ selectedLimiter = (0, express_rate_limit_1.default)(Object.assign({ windowMs: currentWindowMs, max: currentLimit, keyGenerator: (request) => {
134
+ return request.ip || "unknown_ip_for_rate_limit";
135
+ },
136
+ // Pass through other specified options
137
+ message: message, standardHeaders: standardHeaders, legacyHeaders: legacyHeaders, skip: skip, requestPropertyName: requestPropertyName }, otherRateLimitOptions));
138
+ limiterInstanceCache.set(limiterCacheKey, selectedLimiter);
139
+ }
140
+ return selectedLimiter(req, res, next);
141
+ }
142
+ catch (error) {
143
+ console.error(`[DynamicRateLimiter ${req.path}] Error in rate limiting middleware (IP: ${req.ip}):`, error);
144
+ next();
145
+ }
146
+ });
147
+ }
148
+ exports.createDynamicRateLimiter = createDynamicRateLimiter;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { Mongoose, Model, Document } from "mongoose";
3
4
  import { AdminAuthDoc } from "./admin-auth";
4
5
  export interface AdminWebAuthnCredentialAttrs {
@@ -0,0 +1,24 @@
1
+ import mongoose from "mongoose";
2
+ interface RouteRule {
3
+ path: string;
4
+ action: string;
5
+ }
6
+ interface CustomRateLimitAttrs {
7
+ createdAt?: Date;
8
+ ipAddress: string;
9
+ limit: number;
10
+ windowMs: number;
11
+ routes: RouteRule[];
12
+ }
13
+ interface CustomRateLimitDoc extends mongoose.Document {
14
+ createdAt: Date;
15
+ ipAddress: string;
16
+ limit: number;
17
+ windowMs: number;
18
+ routes: RouteRule[];
19
+ }
20
+ interface CustomRateLimitModel extends mongoose.Model<CustomRateLimitDoc> {
21
+ build(attrs: CustomRateLimitAttrs): CustomRateLimitDoc;
22
+ }
23
+ declare const buildCustomRateLimit: (mongoose: typeof mongoose) => CustomRateLimitModel;
24
+ export { buildCustomRateLimit, CustomRateLimitDoc, CustomRateLimitAttrs, RouteRule, };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCustomRateLimit = void 0;
4
+ const buildCustomRateLimit = (mongoose) => {
5
+ // if model is already defined, return it
6
+ if (mongoose.models.CustomRateLimit) {
7
+ return mongoose.model("CustomRateLimit");
8
+ }
9
+ // Define the schema for the nested RouteRule object
10
+ const RouteRuleSchema = new mongoose.Schema({
11
+ path: {
12
+ type: String,
13
+ required: [true, "Path (regex string) is required for a route rule"],
14
+ },
15
+ action: {
16
+ type: String,
17
+ required: [true, "Action (HTTP method) is required for a route rule"],
18
+ uppercase: true, // Store methods consistently (e.g., 'GET')
19
+ // Optional: Add enum validation if you want to restrict methods
20
+ // enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*'] // Example enum
21
+ },
22
+ }, { _id: false }); // Don't create separate IDs for subdocuments
23
+ const CustomRateLimitSchema = new mongoose.Schema({
24
+ createdAt: {
25
+ type: mongoose.Schema.Types.Date,
26
+ default: Date.now, // Add default value for createdAt
27
+ },
28
+ ipAddress: {
29
+ type: String,
30
+ required: true,
31
+ },
32
+ limit: {
33
+ type: Number,
34
+ required: true,
35
+ },
36
+ windowMs: {
37
+ type: Number,
38
+ required: true,
39
+ },
40
+ routes: {
41
+ type: [RouteRuleSchema],
42
+ required: true,
43
+ // Default to empty array, meaning applies to all routes/methods if not specified
44
+ default: [],
45
+ },
46
+ }, {
47
+ toJSON: {
48
+ transform(doc, ret) {
49
+ ret.id = ret._id;
50
+ delete ret._id;
51
+ delete ret.__v;
52
+ },
53
+ },
54
+ });
55
+ CustomRateLimitSchema.statics.build = (attrs) => {
56
+ var _a;
57
+ // Ensure default createdAt is handled if not provided
58
+ return new CustomRateLimit(Object.assign(Object.assign({}, attrs), { createdAt: (_a = attrs.createdAt) !== null && _a !== void 0 ? _a : new Date() }));
59
+ };
60
+ const CustomRateLimit = mongoose.model("CustomRateLimit", CustomRateLimitSchema);
61
+ return CustomRateLimit;
62
+ };
63
+ exports.buildCustomRateLimit = buildCustomRateLimit;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { Mongoose, Model, Document } from "mongoose";
3
4
  import { AuthDoc } from "./auth";
4
5
  export interface WebAuthnCredentialAttrs {
@@ -14,8 +14,10 @@ const winston_1 = require("winston");
14
14
  class LoggerService {
15
15
  constructor() {
16
16
  this.maskSensitiveData = (0, winston_1.format)((info) => {
17
+ var _a, _b, _c, _d, _e;
17
18
  // Mask sensitive headers
18
- if (info.meta && info.meta.req && info.meta.req.headers) {
19
+ if ((_b = (_a = info.meta) === null || _a === void 0 ? void 0 : _a.req) === null || _b === void 0 ? void 0 : _b.headers) {
20
+ // Use optional chaining
19
21
  const headers = info.meta.req.headers;
20
22
  const headerKeysToMask = [
21
23
  "x-api-key",
@@ -24,7 +26,8 @@ class LoggerService {
24
26
  ];
25
27
  headerKeysToMask.forEach((key) => {
26
28
  if (headers[key]) {
27
- headers[key] = this.maskValue(headers[key]); // Use maskValue function
29
+ // Type assertion might be needed if headers[key] is not strictly string
30
+ headers[key] = this.maskValue(String(headers[key]));
28
31
  }
29
32
  });
30
33
  // Mask cookies specifically
@@ -38,22 +41,26 @@ class LoggerService {
38
41
  key === "refreshToken" ||
39
42
  key === "adminRefreshToken" ||
40
43
  key === "phoneVerifiedJWT") {
41
- return `${key}=${this.maskValue(value, true)}`; // Mask the sensitive cookie
44
+ return `${key}=${this.maskValue(value, true)}`;
42
45
  }
43
- return cookie; // Return unmodified if not sensitive
46
+ return cookie;
44
47
  })
45
48
  .join("; ");
46
49
  }
47
50
  }
48
51
  // Mask sensitive request body fields
49
- if (info.meta && info.meta.req && info.meta.req.body) {
52
+ if (((_d = (_c = info.meta) === null || _c === void 0 ? void 0 : _c.req) === null || _d === void 0 ? void 0 : _d.body) &&
53
+ typeof info.meta.req.body === "object" &&
54
+ info.meta.req.body !== null) {
55
+ const requestBody = info.meta.req.body; // Type assertion
50
56
  const requestBodyFieldsToMaskWithLastFour = [
51
57
  "accountNumber",
52
58
  "routingNumber",
53
59
  ];
54
60
  requestBodyFieldsToMaskWithLastFour.forEach((field) => {
55
- if (info.meta.req.body.hasOwnProperty(field)) {
56
- info.meta.req.body[field] = this.maskValue(info.meta.req.body[field], true);
61
+ if (requestBody.hasOwnProperty(field) &&
62
+ typeof requestBody[field] === "string") {
63
+ requestBody[field] = this.maskValue(requestBody[field], true);
57
64
  }
58
65
  });
59
66
  const requestBodyFieldsToMaskWithoutLastFour = [
@@ -64,26 +71,34 @@ class LoggerService {
64
71
  "authenticatorCode",
65
72
  ];
66
73
  requestBodyFieldsToMaskWithoutLastFour.forEach((field) => {
67
- if (info.meta.req.body.hasOwnProperty(field)) {
68
- info.meta.req.body[field] = this.maskValue(info.meta.req.body[field]);
74
+ if (requestBody.hasOwnProperty(field) &&
75
+ typeof requestBody[field] === "string") {
76
+ requestBody[field] = this.maskValue(requestBody[field]);
69
77
  }
70
78
  });
71
79
  }
72
80
  // Mask sensitive response body fields
73
- if (info.meta && info.meta.res && info.meta.res.body) {
81
+ const resAsAny = (_e = info.meta) === null || _e === void 0 ? void 0 : _e.res;
82
+ if ((resAsAny === null || resAsAny === void 0 ? void 0 : resAsAny.body) &&
83
+ typeof resAsAny.body === "object" &&
84
+ resAsAny.body !== null) {
85
+ const responseBody = resAsAny.body; // Keep this assertion for object access
74
86
  const responseBodyFieldsToMask = ["secret", "value"];
75
87
  responseBodyFieldsToMask.forEach((field) => {
76
- if (info.meta.res.body.hasOwnProperty(field)) {
77
- info.meta.res.body[field] = this.maskValue(info.meta.res.body[field]); // Use maskValue function
88
+ if (responseBody.hasOwnProperty(field) &&
89
+ typeof responseBody[field] === "string") {
90
+ responseBody[field] = this.maskValue(responseBody[field]);
78
91
  }
79
92
  });
80
93
  }
81
94
  return info;
82
95
  });
83
96
  this.requestMethodFilter = (0, winston_1.format)((info) => {
97
+ var _a, _b;
84
98
  const allowedMethods = ["POST", "PATCH", "DELETE"];
85
99
  // Only apply filter if req.method is defined
86
- if (info.meta && info.meta.req && info.meta.req.method) {
100
+ if ((_b = (_a = info.meta) === null || _a === void 0 ? void 0 : _a.req) === null || _b === void 0 ? void 0 : _b.method) {
101
+ // Use optional chaining
87
102
  if (allowedMethods.includes(info.meta.req.method)) {
88
103
  return info;
89
104
  }
@@ -96,41 +111,41 @@ class LoggerService {
96
111
  });
97
112
  // Custom format to handle CustomError serialization
98
113
  this.handleCustomErrorFormat = (0, winston_1.format)((info) => {
114
+ var _a, _b, _c;
99
115
  let error;
100
116
  // express-winston errorLogger often puts the error in info.message or info.meta.err
101
117
  if (info.message instanceof Error) {
102
118
  error = info.message;
103
119
  }
104
- else if (info.meta && info.meta.err instanceof Error) {
120
+ else if (((_a = info.meta) === null || _a === void 0 ? void 0 : _a.err) instanceof Error) {
121
+ // Use optional chaining
105
122
  error = info.meta.err;
106
123
  }
107
124
  else if (info.level === "error" &&
108
125
  info.message &&
109
126
  typeof info.message === "object" &&
110
- info.message.stack) {
111
- // Sometimes the error object might be directly in message for non-express-winston logs
127
+ info.message.stack // Type assertion to check for stack
128
+ ) {
112
129
  error = info.message;
113
130
  }
114
131
  if (error instanceof common_1.CustomError) {
115
- // Replace the error object/message with serialized details
116
- // Log serialized errors under 'errorDetails' and keep original message if it's just a string
117
132
  info.errorDetails = error.serializeErrors();
118
133
  if (typeof info.message === "object") {
119
- info.message = error.message; // Keep the original error message string
134
+ info.message = error.message;
120
135
  }
121
- // Prevent Winston from trying to serialize the original complex error object
122
- if (info.meta && info.meta.err) {
136
+ if ((_b = info.meta) === null || _b === void 0 ? void 0 : _b.err) {
137
+ // Use optional chaining
123
138
  delete info.meta.err;
124
139
  }
125
140
  }
126
141
  else if (error) {
127
- // For generic errors, ensure the stack is included if available
128
142
  info.errorMessage = error.message;
129
143
  info.errorStack = error.stack;
130
144
  if (typeof info.message === "object") {
131
145
  info.message = error.message;
132
146
  }
133
- if (info.meta && info.meta.err) {
147
+ if ((_c = info.meta) === null || _c === void 0 ? void 0 : _c.err) {
148
+ // Use optional chaining
134
149
  delete info.meta.err;
135
150
  }
136
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2275",
3
+ "version": "1.0.2277",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -34,6 +34,7 @@
34
34
  "crypto-js": "^4.2.0",
35
35
  "csurf": "^1.11.0",
36
36
  "express": "^4.21.1",
37
+ "express-rate-limit": "^7.5.0",
37
38
  "express-validator": "^6.14.2",
38
39
  "fireblocks-sdk": "^5.13.0",
39
40
  "googleapis": "^135.1.0",