Skip to main content
Glama

Git MCP Server

idGenerator.ts12.3 kB
/** * @fileoverview Provides a utility class `IdGenerator` for creating customizable, prefixed unique identifiers, * and a standalone `generateUUID` function for generating standard UUIDs. * The `IdGenerator` supports entity-specific prefixes, custom character sets, and lengths. * * Note: Logging has been removed from this module to prevent circular dependencies * with the `requestContextService`, which itself uses `generateUUID` from this module. * This was causing `ReferenceError: Cannot access 'generateUUID' before initialization` * during application startup. * @module src/utils/security/idGenerator */ import { randomUUID as cryptoRandomUUID, randomBytes } from 'crypto'; import { JsonRpcErrorCode, McpError } from '@/types-global/errors.js'; // Removed: import { logger, requestContextService } from "../index.js"; /** * Defines the structure for configuring entity prefixes. * Keys are entity type names (e.g., "project", "task"), and values are their corresponding ID prefixes (e.g., "PROJ", "TASK"). */ export interface EntityPrefixConfig { [key: string]: string; } /** * Defines options for customizing ID generation. */ export interface IdGenerationOptions { length?: number; separator?: string; charset?: string; } /** * A generic ID Generator class for creating and managing unique, prefixed identifiers. * Allows defining custom prefixes, generating random strings, and validating/normalizing IDs. */ export class IdGenerator { /** * Default character set for the random part of the ID. * @private */ private static DEFAULT_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; /** * Default separator character between prefix and random part. * @private */ private static DEFAULT_SEPARATOR = '_'; /** * Default length for the random part of the ID. * @private */ private static DEFAULT_LENGTH = 6; /** * Stores the mapping of entity types to their prefixes. * @private */ private entityPrefixes: EntityPrefixConfig = {}; /** * Stores a reverse mapping from prefixes (case-insensitive) to entity types. * @private */ private prefixToEntityType: Record<string, string> = {}; /** * Constructs an `IdGenerator` instance. * @param entityPrefixes - An initial map of entity types to their prefixes. */ constructor(entityPrefixes: EntityPrefixConfig = {}) { // Logging removed to prevent circular dependency with requestContextService. this.setEntityPrefixes(entityPrefixes); } /** * Sets or updates the entity prefix configuration and rebuilds the internal reverse lookup map. * @param entityPrefixes - A map where keys are entity type names and values are their desired ID prefixes. */ public setEntityPrefixes(entityPrefixes: EntityPrefixConfig): void { // Logging removed. this.entityPrefixes = { ...entityPrefixes }; this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce( (acc, [type, prefix]) => { acc[prefix.toLowerCase()] = type; // Store lowercase for case-insensitive lookup return acc; }, {} as Record<string, string>, ); } /** * Retrieves a copy of the current entity prefix configuration. * @returns The current entity prefix configuration. */ public getEntityPrefixes(): EntityPrefixConfig { return { ...this.entityPrefixes }; } /** * Generates a cryptographically secure random string. * @param length - The desired length of the random string. Defaults to `IdGenerator.DEFAULT_LENGTH`. * @param charset - The character set to use. Defaults to `IdGenerator.DEFAULT_CHARSET`. * @returns The generated random string. */ public generateRandomString( length: number = IdGenerator.DEFAULT_LENGTH, charset: string = IdGenerator.DEFAULT_CHARSET, ): string { let result = ''; // Determine the largest multiple of charset.length that is less than or equal to 256 // This is the threshold for rejection sampling to avoid bias. const maxValidByteValue = Math.floor(256 / charset.length) * charset.length; while (result.length < length) { const byteBuffer = randomBytes(1); // Get one random byte const byte = byteBuffer[0]; // If the byte is within the valid range (i.e., it won't introduce bias), // use it to select a character from the charset. Otherwise, discard and try again. if (byte !== undefined && byte < maxValidByteValue) { const charIndex = byte % charset.length; const char = charset[charIndex]; if (char) { result += char; } } } return result; } /** * Generates a unique ID, optionally prepended with a prefix. * @param prefix - An optional prefix for the ID. * @param options - Optional parameters for ID generation (length, separator, charset). * @returns A unique identifier string. */ public generate(prefix?: string, options: IdGenerationOptions = {}): string { // Logging removed. const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, } = options; const randomPart = this.generateRandomString(length, charset); const generatedId = prefix ? `${prefix}${separator}${randomPart}` : randomPart; return generatedId; } /** * Generates a unique ID for a specified entity type, using its configured prefix. * @param entityType - The type of entity (must be registered). * @param options - Optional parameters for ID generation. * @returns A unique identifier string for the entity (e.g., "PROJ_A6B3J0"). * @throws {McpError} If the `entityType` is not registered. */ public generateForEntity( entityType: string, options: IdGenerationOptions = {}, ): string { const prefix = this.entityPrefixes[entityType]; if (!prefix) { throw new McpError( JsonRpcErrorCode.ValidationError, `Unknown entity type: ${entityType}. No prefix registered.`, ); } return this.generate(prefix, options); } /** * Validates if an ID conforms to the expected format for a specific entity type. * @param id - The ID string to validate. * @param entityType - The expected entity type of the ID. * @param options - Optional parameters used during generation for validation consistency. * The `charset` from these options will be used for validation. * @returns `true` if the ID is valid, `false` otherwise. */ public isValid( id: string, entityType: string, options: IdGenerationOptions = {}, ): boolean { const prefix = this.entityPrefixes[entityType]; const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, // Use charset from options or default } = options; if (!prefix) { return false; } // Build regex character class from the charset // Escape characters that have special meaning inside a regex character class `[]` const escapedCharsetForClass = charset.replace(/[[\]\\^-]/g, '\\$&'); const charsetRegexPart = `[${escapedCharsetForClass}]`; const pattern = new RegExp( `^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}${charsetRegexPart}{${length}}$`, ); return pattern.test(id); } /** * Escapes special characters in a string for use in a regular expression. * @param str - The string to escape. * @returns The escaped string. * @private */ private escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Strips the prefix and separator from an ID string. * @param id - The ID string (e.g., "PROJ_A6B3J0"). * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`. * @returns The ID part without the prefix, or the original ID if separator not found. */ public stripPrefix( id: string, separator: string = IdGenerator.DEFAULT_SEPARATOR, ): string { const parts = id.split(separator); return parts.length > 1 ? parts.slice(1).join(separator) : id; // Handle separators in random part } /** * Determines the entity type from an ID string by its prefix (case-insensitive). * @param id - The ID string (e.g., "PROJ_A6B3J0"). * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`. * @returns The determined entity type. * @throws {McpError} If ID format is invalid or prefix is unknown. */ public getEntityType( id: string, separator: string = IdGenerator.DEFAULT_SEPARATOR, ): string { const parts = id.split(separator); if (parts.length < 2 || !parts[0]) { throw new McpError( JsonRpcErrorCode.ValidationError, `Invalid ID format: ${id}. Expected format like: PREFIX${separator}RANDOMLPART`, ); } const prefix = parts[0]; const entityType = this.prefixToEntityType[prefix.toLowerCase()]; if (!entityType) { throw new McpError( JsonRpcErrorCode.ValidationError, `Unknown entity type for prefix: ${prefix}`, ); } return entityType; } /** * Normalizes an entity ID to ensure the prefix matches the registered case * and the random part is uppercase. Note: This assumes the charset characters * have a meaningful uppercase version if case-insensitivity is desired for the random part. * For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary. * @param id - The ID to normalize (e.g., "proj_a6b3j0"). * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`. * @returns The normalized ID (e.g., "PROJ_A6B3J0"). * @throws {McpError} If the entity type cannot be determined from the ID. */ public normalize( id: string, separator: string = IdGenerator.DEFAULT_SEPARATOR, ): string { const entityType = this.getEntityType(id, separator); const registeredPrefix = this.entityPrefixes[entityType]; const idParts = id.split(separator); const randomPart = idParts.slice(1).join(separator); // Consider if randomPart.toUpperCase() is always correct for custom charsets. // For now, maintaining existing behavior. return `${registeredPrefix}${separator}${randomPart.toUpperCase()}`; } } /** * Default singleton instance of the `IdGenerator`. * Initialize with `idGenerator.setEntityPrefixes({})` to configure. */ export const idGenerator = new IdGenerator(); /** * Generates a standard Version 4 UUID (Universally Unique Identifier). * Uses the Node.js `crypto` module. * @returns A new UUID string. */ export const generateUUID = (): string => { return cryptoRandomUUID(); }; /** * Generates a unique 10-character alphanumeric ID with a hyphen in the middle (e.g., `ABCDE-FGHIJ`). * This function is specifically for request contexts to provide a shorter, more readable ID. * It contains its own random string generation logic to remain self-contained and avoid circular dependencies. * @returns A new unique ID string. */ export const generateRequestContextId = (): string => { /** * Generates a cryptographically secure random string of a given length from a given charset. * @param length The desired length of the string. * @param charset The characters to use for generation. * @returns The generated random string. */ const generateSecureRandomString = ( length: number, charset: string, ): string => { let result = ''; const maxValidByteValue = Math.floor(256 / charset.length) * charset.length; while (result.length < length) { const byteBuffer = randomBytes(1); const byte = byteBuffer[0]; if (byte !== undefined && byte < maxValidByteValue) { const charIndex = byte % charset.length; const char = charset[charIndex]; if (char) { result += char; } } } return result; }; const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const part1 = generateSecureRandomString(5, charset); const part2 = generateSecureRandomString(5, charset); return `${part1}-${part2}`; };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/git-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server