@cyanheads/pubchem-mcp-server 1.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.
- package/LICENSE +201 -0
- package/README.md +236 -0
- package/dist/config/index.d.ts +98 -0
- package/dist/config/index.js +326 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +214 -0
- package/dist/mcp-server/server.d.ts +21 -0
- package/dist/mcp-server/server.js +124 -0
- package/dist/mcp-server/tools/fetchAssaySummary/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchAssaySummary/index.js +7 -0
- package/dist/mcp-server/tools/fetchAssaySummary/logic.d.ts +71 -0
- package/dist/mcp-server/tools/fetchAssaySummary/logic.js +96 -0
- package/dist/mcp-server/tools/fetchAssaySummary/registration.d.ts +12 -0
- package/dist/mcp-server/tools/fetchAssaySummary/registration.js +68 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/index.js +7 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/logic.d.ts +131 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/logic.js +100 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/registration.d.ts +13 -0
- package/dist/mcp-server/tools/fetchCompoundProperties/registration.js +52 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/index.js +7 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/logic.d.ts +89 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/logic.js +145 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/registration.d.ts +13 -0
- package/dist/mcp-server/tools/fetchCompoundXrefs/registration.js +52 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/index.d.ts +7 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/index.js +7 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/logic.d.ts +69 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/logic.js +97 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/registration.d.ts +13 -0
- package/dist/mcp-server/tools/fetchSubstanceDetails/registration.js +79 -0
- package/dist/mcp-server/tools/getCompoundImage/index.d.ts +7 -0
- package/dist/mcp-server/tools/getCompoundImage/index.js +7 -0
- package/dist/mcp-server/tools/getCompoundImage/logic.d.ts +42 -0
- package/dist/mcp-server/tools/getCompoundImage/logic.js +68 -0
- package/dist/mcp-server/tools/getCompoundImage/registration.d.ts +12 -0
- package/dist/mcp-server/tools/getCompoundImage/registration.js +57 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/index.js +7 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/logic.d.ts +35 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/logic.js +64 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchAssaysByTarget/registration.js +62 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/logic.d.ts +35 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/logic.js +59 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchCompoundByIdentifier/registration.js +51 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/logic.d.ts +38 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/logic.js +75 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchCompoundsByFormula/registration.js +62 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/logic.d.ts +41 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/logic.js +82 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/registration.d.ts +12 -0
- package/dist/mcp-server/tools/searchCompoundsBySimilarity/registration.js +62 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/index.d.ts +7 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/index.js +7 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/logic.d.ts +41 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/logic.js +78 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/registration.d.ts +13 -0
- package/dist/mcp-server/tools/searchCompoundsByStructure/registration.js +63 -0
- package/dist/mcp-server/transports/auth/core/authContext.d.ts +33 -0
- package/dist/mcp-server/transports/auth/core/authContext.js +24 -0
- package/dist/mcp-server/transports/auth/core/authTypes.d.ts +17 -0
- package/dist/mcp-server/transports/auth/core/authTypes.js +5 -0
- package/dist/mcp-server/transports/auth/core/authUtils.d.ts +18 -0
- package/dist/mcp-server/transports/auth/core/authUtils.js +45 -0
- package/dist/mcp-server/transports/auth/index.d.ts +10 -0
- package/dist/mcp-server/transports/auth/index.js +9 -0
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +27 -0
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +149 -0
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +20 -0
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +124 -0
- package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
- package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +21 -0
- package/dist/mcp-server/transports/httpTransport.js +208 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
- package/dist/mcp-server/transports/stdioTransport.js +63 -0
- package/dist/services/pubchem/pubchemApiClient.d.ts +52 -0
- package/dist/services/pubchem/pubchemApiClient.js +120 -0
- package/dist/types-global/errors.d.ts +127 -0
- package/dist/types-global/errors.js +126 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.js +20 -0
- package/dist/utils/internal/errorHandler.d.ts +176 -0
- package/dist/utils/internal/errorHandler.js +335 -0
- package/dist/utils/internal/index.d.ts +9 -0
- package/dist/utils/internal/index.js +9 -0
- package/dist/utils/internal/logger.d.ts +148 -0
- package/dist/utils/internal/logger.js +430 -0
- package/dist/utils/internal/requestContext.d.ts +83 -0
- package/dist/utils/internal/requestContext.js +72 -0
- package/dist/utils/metrics/index.d.ts +7 -0
- package/dist/utils/metrics/index.js +7 -0
- package/dist/utils/metrics/tokenCounter.d.ts +35 -0
- package/dist/utils/metrics/tokenCounter.js +110 -0
- package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
- package/dist/utils/network/fetchWithTimeout.js +59 -0
- package/dist/utils/network/index.d.ts +6 -0
- package/dist/utils/network/index.js +5 -0
- package/dist/utils/parsing/dateParser.d.ts +73 -0
- package/dist/utils/parsing/dateParser.js +107 -0
- package/dist/utils/parsing/index.d.ts +8 -0
- package/dist/utils/parsing/index.js +8 -0
- package/dist/utils/parsing/jsonParser.d.ts +82 -0
- package/dist/utils/parsing/jsonParser.js +126 -0
- package/dist/utils/scheduling/index.d.ts +6 -0
- package/dist/utils/scheduling/index.js +6 -0
- package/dist/utils/scheduling/scheduler.d.ts +72 -0
- package/dist/utils/scheduling/scheduler.js +152 -0
- package/dist/utils/security/idGenerator.d.ts +137 -0
- package/dist/utils/security/idGenerator.js +215 -0
- package/dist/utils/security/index.d.ts +9 -0
- package/dist/utils/security/index.js +9 -0
- package/dist/utils/security/rateLimiter.d.ts +108 -0
- package/dist/utils/security/rateLimiter.js +179 -0
- package/dist/utils/security/sanitization.d.ts +184 -0
- package/dist/utils/security/sanitization.js +453 -0
- package/package.json +108 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a utility class `IdGenerator` for creating customizable, prefixed unique identifiers,
|
|
3
|
+
* and a standalone `generateUUID` function for generating standard UUIDs.
|
|
4
|
+
* The `IdGenerator` supports entity-specific prefixes, custom character sets, and lengths.
|
|
5
|
+
*
|
|
6
|
+
* Note: Logging has been removed from this module to prevent circular dependencies
|
|
7
|
+
* with the `requestContextService`, which itself uses `generateUUID` from this module.
|
|
8
|
+
* This was causing `ReferenceError: Cannot access 'generateUUID' before initialization`
|
|
9
|
+
* during application startup.
|
|
10
|
+
* @module src/utils/security/idGenerator
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID as cryptoRandomUUID, randomBytes } from "crypto";
|
|
13
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
14
|
+
/**
|
|
15
|
+
* A generic ID Generator class for creating and managing unique, prefixed identifiers.
|
|
16
|
+
* Allows defining custom prefixes, generating random strings, and validating/normalizing IDs.
|
|
17
|
+
*/
|
|
18
|
+
export class IdGenerator {
|
|
19
|
+
/**
|
|
20
|
+
* Constructs an `IdGenerator` instance.
|
|
21
|
+
* @param entityPrefixes - An initial map of entity types to their prefixes.
|
|
22
|
+
*/
|
|
23
|
+
constructor(entityPrefixes = {}) {
|
|
24
|
+
/**
|
|
25
|
+
* Stores the mapping of entity types to their prefixes.
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
this.entityPrefixes = {};
|
|
29
|
+
/**
|
|
30
|
+
* Stores a reverse mapping from prefixes (case-insensitive) to entity types.
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
this.prefixToEntityType = {};
|
|
34
|
+
// Logging removed to prevent circular dependency with requestContextService.
|
|
35
|
+
this.setEntityPrefixes(entityPrefixes);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sets or updates the entity prefix configuration and rebuilds the internal reverse lookup map.
|
|
39
|
+
* @param entityPrefixes - A map where keys are entity type names and values are their desired ID prefixes.
|
|
40
|
+
*/
|
|
41
|
+
setEntityPrefixes(entityPrefixes) {
|
|
42
|
+
// Logging removed.
|
|
43
|
+
this.entityPrefixes = { ...entityPrefixes };
|
|
44
|
+
this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce((acc, [type, prefix]) => {
|
|
45
|
+
acc[prefix.toLowerCase()] = type; // Store lowercase for case-insensitive lookup
|
|
46
|
+
return acc;
|
|
47
|
+
}, {});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves a copy of the current entity prefix configuration.
|
|
51
|
+
* @returns The current entity prefix configuration.
|
|
52
|
+
*/
|
|
53
|
+
getEntityPrefixes() {
|
|
54
|
+
return { ...this.entityPrefixes };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generates a cryptographically secure random string.
|
|
58
|
+
* @param length - The desired length of the random string. Defaults to `IdGenerator.DEFAULT_LENGTH`.
|
|
59
|
+
* @param charset - The character set to use. Defaults to `IdGenerator.DEFAULT_CHARSET`.
|
|
60
|
+
* @returns The generated random string.
|
|
61
|
+
*/
|
|
62
|
+
generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) {
|
|
63
|
+
let result = "";
|
|
64
|
+
// Determine the largest multiple of charset.length that is less than or equal to 256
|
|
65
|
+
// This is the threshold for rejection sampling to avoid bias.
|
|
66
|
+
const maxValidByteValue = Math.floor(256 / charset.length) * charset.length;
|
|
67
|
+
while (result.length < length) {
|
|
68
|
+
const byteBuffer = randomBytes(1); // Get one random byte
|
|
69
|
+
const byte = byteBuffer[0];
|
|
70
|
+
// If the byte is within the valid range (i.e., it won't introduce bias),
|
|
71
|
+
// use it to select a character from the charset. Otherwise, discard and try again.
|
|
72
|
+
if (byte < maxValidByteValue) {
|
|
73
|
+
result += charset[byte % charset.length];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generates a unique ID, optionally prepended with a prefix.
|
|
80
|
+
* @param prefix - An optional prefix for the ID.
|
|
81
|
+
* @param options - Optional parameters for ID generation (length, separator, charset).
|
|
82
|
+
* @returns A unique identifier string.
|
|
83
|
+
*/
|
|
84
|
+
generate(prefix, options = {}) {
|
|
85
|
+
// Logging removed.
|
|
86
|
+
const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, } = options;
|
|
87
|
+
const randomPart = this.generateRandomString(length, charset);
|
|
88
|
+
const generatedId = prefix
|
|
89
|
+
? `${prefix}${separator}${randomPart}`
|
|
90
|
+
: randomPart;
|
|
91
|
+
return generatedId;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Generates a unique ID for a specified entity type, using its configured prefix.
|
|
95
|
+
* @param entityType - The type of entity (must be registered).
|
|
96
|
+
* @param options - Optional parameters for ID generation.
|
|
97
|
+
* @returns A unique identifier string for the entity (e.g., "PROJ_A6B3J0").
|
|
98
|
+
* @throws {McpError} If the `entityType` is not registered.
|
|
99
|
+
*/
|
|
100
|
+
generateForEntity(entityType, options = {}) {
|
|
101
|
+
const prefix = this.entityPrefixes[entityType];
|
|
102
|
+
if (!prefix) {
|
|
103
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type: ${entityType}. No prefix registered.`);
|
|
104
|
+
}
|
|
105
|
+
return this.generate(prefix, options);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validates if an ID conforms to the expected format for a specific entity type.
|
|
109
|
+
* @param id - The ID string to validate.
|
|
110
|
+
* @param entityType - The expected entity type of the ID.
|
|
111
|
+
* @param options - Optional parameters used during generation for validation consistency.
|
|
112
|
+
* The `charset` from these options will be used for validation.
|
|
113
|
+
* @returns `true` if the ID is valid, `false` otherwise.
|
|
114
|
+
*/
|
|
115
|
+
isValid(id, entityType, options = {}) {
|
|
116
|
+
const prefix = this.entityPrefixes[entityType];
|
|
117
|
+
const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, // Use charset from options or default
|
|
118
|
+
} = options;
|
|
119
|
+
if (!prefix) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Build regex character class from the charset
|
|
123
|
+
// Escape characters that have special meaning inside a regex character class `[]`
|
|
124
|
+
const escapedCharsetForClass = charset.replace(/[[\]\\^-]/g, "\\$&");
|
|
125
|
+
const charsetRegexPart = `[${escapedCharsetForClass}]`;
|
|
126
|
+
const pattern = new RegExp(`^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}${charsetRegexPart}{${length}}$`);
|
|
127
|
+
return pattern.test(id);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Escapes special characters in a string for use in a regular expression.
|
|
131
|
+
* @param str - The string to escape.
|
|
132
|
+
* @returns The escaped string.
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
escapeRegex(str) {
|
|
136
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Strips the prefix and separator from an ID string.
|
|
140
|
+
* @param id - The ID string (e.g., "PROJ_A6B3J0").
|
|
141
|
+
* @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
|
|
142
|
+
* @returns The ID part without the prefix, or the original ID if separator not found.
|
|
143
|
+
*/
|
|
144
|
+
stripPrefix(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
|
|
145
|
+
const parts = id.split(separator);
|
|
146
|
+
return parts.length > 1 ? parts.slice(1).join(separator) : id; // Handle separators in random part
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Determines the entity type from an ID string by its prefix (case-insensitive).
|
|
150
|
+
* @param id - The ID string (e.g., "PROJ_A6B3J0").
|
|
151
|
+
* @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
|
|
152
|
+
* @returns The determined entity type.
|
|
153
|
+
* @throws {McpError} If ID format is invalid or prefix is unknown.
|
|
154
|
+
*/
|
|
155
|
+
getEntityType(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
|
|
156
|
+
const parts = id.split(separator);
|
|
157
|
+
if (parts.length < 2 || !parts[0]) {
|
|
158
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid ID format: ${id}. Expected format like: PREFIX${separator}RANDOMLPART`);
|
|
159
|
+
}
|
|
160
|
+
const prefix = parts[0];
|
|
161
|
+
const entityType = this.prefixToEntityType[prefix.toLowerCase()];
|
|
162
|
+
if (!entityType) {
|
|
163
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type for prefix: ${prefix}`);
|
|
164
|
+
}
|
|
165
|
+
return entityType;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Normalizes an entity ID to ensure the prefix matches the registered case
|
|
169
|
+
* and the random part is uppercase. Note: This assumes the charset characters
|
|
170
|
+
* have a meaningful uppercase version if case-insensitivity is desired for the random part.
|
|
171
|
+
* For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary.
|
|
172
|
+
* @param id - The ID to normalize (e.g., "proj_a6b3j0").
|
|
173
|
+
* @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
|
|
174
|
+
* @returns The normalized ID (e.g., "PROJ_A6B3J0").
|
|
175
|
+
* @throws {McpError} If the entity type cannot be determined from the ID.
|
|
176
|
+
*/
|
|
177
|
+
normalize(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
|
|
178
|
+
const entityType = this.getEntityType(id, separator);
|
|
179
|
+
const registeredPrefix = this.entityPrefixes[entityType];
|
|
180
|
+
const idParts = id.split(separator);
|
|
181
|
+
const randomPart = idParts.slice(1).join(separator);
|
|
182
|
+
// Consider if randomPart.toUpperCase() is always correct for custom charsets.
|
|
183
|
+
// For now, maintaining existing behavior.
|
|
184
|
+
return `${registeredPrefix}${separator}${randomPart.toUpperCase()}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Default character set for the random part of the ID.
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
IdGenerator.DEFAULT_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
192
|
+
/**
|
|
193
|
+
* Default separator character between prefix and random part.
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
IdGenerator.DEFAULT_SEPARATOR = "_";
|
|
197
|
+
/**
|
|
198
|
+
* Default length for the random part of the ID.
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
IdGenerator.DEFAULT_LENGTH = 6;
|
|
202
|
+
/**
|
|
203
|
+
* Default singleton instance of the `IdGenerator`.
|
|
204
|
+
* Initialize with `idGenerator.setEntityPrefixes({})` to configure.
|
|
205
|
+
*/
|
|
206
|
+
export const idGenerator = new IdGenerator();
|
|
207
|
+
/**
|
|
208
|
+
* Generates a standard Version 4 UUID (Universally Unique Identifier).
|
|
209
|
+
* Uses the Node.js `crypto` module. This function is independent of the IdGenerator instance
|
|
210
|
+
* to prevent circular dependencies when used by other utilities like requestContextService.
|
|
211
|
+
* @returns A new UUID string.
|
|
212
|
+
*/
|
|
213
|
+
export const generateUUID = () => {
|
|
214
|
+
return cryptoRandomUUID();
|
|
215
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for security-related utility modules.
|
|
3
|
+
* This file re-exports utilities for input sanitization, rate limiting,
|
|
4
|
+
* and ID generation.
|
|
5
|
+
* @module src/utils/security
|
|
6
|
+
*/
|
|
7
|
+
export * from "./idGenerator.js";
|
|
8
|
+
export * from "./rateLimiter.js";
|
|
9
|
+
export * from "./sanitization.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for security-related utility modules.
|
|
3
|
+
* This file re-exports utilities for input sanitization, rate limiting,
|
|
4
|
+
* and ID generation.
|
|
5
|
+
* @module src/utils/security
|
|
6
|
+
*/
|
|
7
|
+
export * from "./idGenerator.js";
|
|
8
|
+
export * from "./rateLimiter.js";
|
|
9
|
+
export * from "./sanitization.js";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { RequestContext } from "../index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Defines configuration options for the {@link RateLimiter}.
|
|
4
|
+
*/
|
|
5
|
+
export interface RateLimitConfig {
|
|
6
|
+
/** Time window in milliseconds. */
|
|
7
|
+
windowMs: number;
|
|
8
|
+
/** Maximum number of requests allowed in the window. */
|
|
9
|
+
maxRequests: number;
|
|
10
|
+
/** Custom error message template. Can include `{waitTime}` placeholder. */
|
|
11
|
+
errorMessage?: string;
|
|
12
|
+
/** If true, skip rate limiting in development. */
|
|
13
|
+
skipInDevelopment?: boolean;
|
|
14
|
+
/** Optional function to generate a custom key for rate limiting. */
|
|
15
|
+
keyGenerator?: (identifier: string, context?: RequestContext) => string;
|
|
16
|
+
/** How often, in milliseconds, to clean up expired entries. */
|
|
17
|
+
cleanupInterval?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Represents an individual entry for tracking requests against a rate limit key.
|
|
21
|
+
*/
|
|
22
|
+
export interface RateLimitEntry {
|
|
23
|
+
/** Current request count. */
|
|
24
|
+
count: number;
|
|
25
|
+
/** When the window resets (timestamp in milliseconds). */
|
|
26
|
+
resetTime: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A generic rate limiter class using an in-memory store.
|
|
30
|
+
* Controls frequency of operations based on unique keys.
|
|
31
|
+
*/
|
|
32
|
+
export declare class RateLimiter {
|
|
33
|
+
private config;
|
|
34
|
+
/**
|
|
35
|
+
* Stores current request counts and reset times for each key.
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
private limits;
|
|
39
|
+
/**
|
|
40
|
+
* Timer ID for periodic cleanup.
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
private cleanupTimer;
|
|
44
|
+
/**
|
|
45
|
+
* Default configuration values.
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
private static DEFAULT_CONFIG;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new `RateLimiter` instance.
|
|
51
|
+
* @param config - Configuration options, merged with defaults.
|
|
52
|
+
*/
|
|
53
|
+
constructor(config: RateLimitConfig);
|
|
54
|
+
/**
|
|
55
|
+
* Starts the periodic timer to clean up expired rate limit entries.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
private startCleanupTimer;
|
|
59
|
+
/**
|
|
60
|
+
* Removes expired rate limit entries from the store.
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
private cleanupExpiredEntries;
|
|
64
|
+
/**
|
|
65
|
+
* Updates the configuration of the rate limiter instance.
|
|
66
|
+
* @param config - New configuration options to merge.
|
|
67
|
+
*/
|
|
68
|
+
configure(config: Partial<RateLimitConfig>): void;
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves a copy of the current rate limiter configuration.
|
|
71
|
+
* @returns The current configuration.
|
|
72
|
+
*/
|
|
73
|
+
getConfig(): RateLimitConfig;
|
|
74
|
+
/**
|
|
75
|
+
* Resets all rate limits by clearing the internal store.
|
|
76
|
+
*/
|
|
77
|
+
reset(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Checks if a request exceeds the configured rate limit.
|
|
80
|
+
* Throws an `McpError` if the limit is exceeded.
|
|
81
|
+
*
|
|
82
|
+
* @param key - A unique identifier for the request source.
|
|
83
|
+
* @param context - Optional request context for custom key generation.
|
|
84
|
+
* @throws {McpError} If the rate limit is exceeded.
|
|
85
|
+
*/
|
|
86
|
+
check(key: string, context?: RequestContext): void;
|
|
87
|
+
/**
|
|
88
|
+
* Retrieves the current rate limit status for a specific key.
|
|
89
|
+
* @param key - The rate limit key.
|
|
90
|
+
* @returns Status object or `null` if no entry exists.
|
|
91
|
+
*/
|
|
92
|
+
getStatus(key: string): {
|
|
93
|
+
current: number;
|
|
94
|
+
limit: number;
|
|
95
|
+
remaining: number;
|
|
96
|
+
resetTime: number;
|
|
97
|
+
} | null;
|
|
98
|
+
/**
|
|
99
|
+
* Stops the cleanup timer and clears all rate limit entries.
|
|
100
|
+
* Call when the rate limiter is no longer needed.
|
|
101
|
+
*/
|
|
102
|
+
dispose(): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Default singleton instance of the `RateLimiter`.
|
|
106
|
+
* Initialized with default configuration. Use `rateLimiter.configure({})` to customize.
|
|
107
|
+
*/
|
|
108
|
+
export declare const rateLimiter: RateLimiter;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a generic `RateLimiter` class for implementing rate limiting logic.
|
|
3
|
+
* It supports configurable time windows, request limits, and automatic cleanup of expired entries.
|
|
4
|
+
* @module src/utils/security/rateLimiter
|
|
5
|
+
*/
|
|
6
|
+
import { environment } from "../../config/index.js";
|
|
7
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
8
|
+
import { logger, requestContextService } from "../index.js";
|
|
9
|
+
/**
|
|
10
|
+
* A generic rate limiter class using an in-memory store.
|
|
11
|
+
* Controls frequency of operations based on unique keys.
|
|
12
|
+
*/
|
|
13
|
+
export class RateLimiter {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new `RateLimiter` instance.
|
|
16
|
+
* @param config - Configuration options, merged with defaults.
|
|
17
|
+
*/
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
/**
|
|
21
|
+
* Timer ID for periodic cleanup.
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
this.cleanupTimer = null;
|
|
25
|
+
this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config };
|
|
26
|
+
this.limits = new Map();
|
|
27
|
+
this.startCleanupTimer();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Starts the periodic timer to clean up expired rate limit entries.
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
startCleanupTimer() {
|
|
34
|
+
if (this.cleanupTimer) {
|
|
35
|
+
clearInterval(this.cleanupTimer);
|
|
36
|
+
}
|
|
37
|
+
const interval = this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval;
|
|
38
|
+
if (interval && interval > 0) {
|
|
39
|
+
this.cleanupTimer = setInterval(() => {
|
|
40
|
+
this.cleanupExpiredEntries();
|
|
41
|
+
}, interval);
|
|
42
|
+
if (this.cleanupTimer.unref) {
|
|
43
|
+
this.cleanupTimer.unref(); // Allow Node.js process to exit if only timer active
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Removes expired rate limit entries from the store.
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
cleanupExpiredEntries() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
let expiredCount = 0;
|
|
54
|
+
for (const [key, entry] of this.limits.entries()) {
|
|
55
|
+
if (now >= entry.resetTime) {
|
|
56
|
+
this.limits.delete(key);
|
|
57
|
+
expiredCount++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (expiredCount > 0) {
|
|
61
|
+
const logContext = requestContextService.createRequestContext({
|
|
62
|
+
operation: "RateLimiter.cleanupExpiredEntries",
|
|
63
|
+
cleanedCount: expiredCount,
|
|
64
|
+
totalRemainingAfterClean: this.limits.size,
|
|
65
|
+
});
|
|
66
|
+
logger.debug(`Cleaned up ${expiredCount} expired rate limit entries`, logContext);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Updates the configuration of the rate limiter instance.
|
|
71
|
+
* @param config - New configuration options to merge.
|
|
72
|
+
*/
|
|
73
|
+
configure(config) {
|
|
74
|
+
this.config = { ...this.config, ...config };
|
|
75
|
+
if (config.cleanupInterval !== undefined) {
|
|
76
|
+
this.startCleanupTimer();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Retrieves a copy of the current rate limiter configuration.
|
|
81
|
+
* @returns The current configuration.
|
|
82
|
+
*/
|
|
83
|
+
getConfig() {
|
|
84
|
+
return { ...this.config };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resets all rate limits by clearing the internal store.
|
|
88
|
+
*/
|
|
89
|
+
reset() {
|
|
90
|
+
this.limits.clear();
|
|
91
|
+
const logContext = requestContextService.createRequestContext({
|
|
92
|
+
operation: "RateLimiter.reset",
|
|
93
|
+
});
|
|
94
|
+
logger.debug("Rate limiter reset, all limits cleared", logContext);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Checks if a request exceeds the configured rate limit.
|
|
98
|
+
* Throws an `McpError` if the limit is exceeded.
|
|
99
|
+
*
|
|
100
|
+
* @param key - A unique identifier for the request source.
|
|
101
|
+
* @param context - Optional request context for custom key generation.
|
|
102
|
+
* @throws {McpError} If the rate limit is exceeded.
|
|
103
|
+
*/
|
|
104
|
+
check(key, context) {
|
|
105
|
+
if (this.config.skipInDevelopment && environment === "development") {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const limitKey = this.config.keyGenerator
|
|
109
|
+
? this.config.keyGenerator(key, context)
|
|
110
|
+
: key;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const entry = this.limits.get(limitKey);
|
|
113
|
+
if (!entry || now >= entry.resetTime) {
|
|
114
|
+
this.limits.set(limitKey, {
|
|
115
|
+
count: 1,
|
|
116
|
+
resetTime: now + this.config.windowMs,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (entry.count >= this.config.maxRequests) {
|
|
121
|
+
const waitTime = Math.ceil((entry.resetTime - now) / 1000);
|
|
122
|
+
const errorMessage = (this.config.errorMessage || RateLimiter.DEFAULT_CONFIG.errorMessage).replace("{waitTime}", waitTime.toString());
|
|
123
|
+
throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, {
|
|
124
|
+
waitTimeSeconds: waitTime,
|
|
125
|
+
key: limitKey,
|
|
126
|
+
limit: this.config.maxRequests,
|
|
127
|
+
windowMs: this.config.windowMs,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
entry.count++;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Retrieves the current rate limit status for a specific key.
|
|
134
|
+
* @param key - The rate limit key.
|
|
135
|
+
* @returns Status object or `null` if no entry exists.
|
|
136
|
+
*/
|
|
137
|
+
getStatus(key) {
|
|
138
|
+
const entry = this.limits.get(key);
|
|
139
|
+
if (!entry) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
current: entry.count,
|
|
144
|
+
limit: this.config.maxRequests,
|
|
145
|
+
remaining: Math.max(0, this.config.maxRequests - entry.count),
|
|
146
|
+
resetTime: entry.resetTime,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Stops the cleanup timer and clears all rate limit entries.
|
|
151
|
+
* Call when the rate limiter is no longer needed.
|
|
152
|
+
*/
|
|
153
|
+
dispose() {
|
|
154
|
+
if (this.cleanupTimer) {
|
|
155
|
+
clearInterval(this.cleanupTimer);
|
|
156
|
+
this.cleanupTimer = null;
|
|
157
|
+
}
|
|
158
|
+
this.limits.clear();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Default configuration values.
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
RateLimiter.DEFAULT_CONFIG = {
|
|
166
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
167
|
+
maxRequests: 100,
|
|
168
|
+
errorMessage: "Rate limit exceeded. Please try again in {waitTime} seconds.",
|
|
169
|
+
skipInDevelopment: false,
|
|
170
|
+
cleanupInterval: 5 * 60 * 1000, // 5 minutes
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Default singleton instance of the `RateLimiter`.
|
|
174
|
+
* Initialized with default configuration. Use `rateLimiter.configure({})` to customize.
|
|
175
|
+
*/
|
|
176
|
+
export const rateLimiter = new RateLimiter({
|
|
177
|
+
windowMs: 15 * 60 * 1000, // Default: 15 minutes
|
|
178
|
+
maxRequests: 100, // Default: 100 requests per window
|
|
179
|
+
});
|