UNPKG

mcp-secrets-vault

Version:

MCP-compatible tool for secure secret management without exposure

1,331 lines (1,318 loc) 103 kB
#!/usr/bin/env node // src/utils/package-info.ts import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; var __filename = fileURLToPath(import.meta.url); var __dirname = dirname(__filename); function findPackageJson() { const possiblePaths = [ join(__dirname, "..", "..", "package.json"), // From src/utils join(__dirname, "..", "package.json"), // From dist join(process.cwd(), "package.json") // From current working directory ]; for (const path3 of possiblePaths) { try { return JSON.parse(readFileSync(path3, "utf-8")); } catch { } } return { version: "0.1.1", name: "mcp-secrets-vault" }; } var packageJson = findPackageJson(); var PACKAGE_VERSION = packageJson.version; var PACKAGE_NAME = packageJson.name; // src/constants/config-constants.ts var CONFIG = { // Version VERSION: PACKAGE_VERSION, // Server settings SERVER_NAME: PACKAGE_NAME, SERVER_VERSION: PACKAGE_VERSION, PROTOCOL_VERSION: "1.0.0", // HTTP settings HTTP_TIMEOUT_MS: 3e4, HTTP_MAX_REDIRECTS: 5, HTTP_DEFAULT_USER_AGENT: `MCP-Secrets-Vault/${PACKAGE_VERSION}`, // Rate limiting DEFAULT_RATE_LIMIT_REQUESTS: 100, DEFAULT_RATE_LIMIT_WINDOW_SECONDS: 3600, RATE_LIMIT_CLEANUP_INTERVAL_MS: 6e4, MILLISECONDS_PER_SECOND: 1e3, RATE_LIMIT_WINDOW_MULTIPLIER: 2, // Audit settings AUDIT_FILE_PREFIX: "audit", AUDIT_FILE_EXTENSION: ".jsonl", AUDIT_MAX_FILE_SIZE_MB: 100, AUDIT_MAX_FILE_AGE_DAYS: 30, AUDIT_ROTATION_CHECK_INTERVAL_MS: 36e5, AUDIT_DEFAULT_PAGE_SIZE: 50, AUDIT_MAX_PAGE_SIZE: 500, // Response limits RESPONSE_MAX_BODY_LENGTH: 1e4, RESPONSE_TRUNCATION_MESSAGE: "... [truncated]", // Sanitization - Comprehensive patterns for defense in depth SANITIZE_AUTH_HEADER_PATTERN: /authorization/i, SANITIZE_SECRET_PATTERN: /\b(api[_-]?key|token|password|auth|bearer|credential|private[_-]?key|access[_-]?key)\b/i, SANITIZE_REPLACEMENT: "[REDACTED]", // Additional redaction patterns for security hardening REDACT_URL_AUTH_PATTERN: /https?:\/\/[^:]+:[^@]+@[^\s]+/gi, REDACT_JWT_PATTERN: /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, REDACT_BEARER_TOKEN_PATTERN: /bearer\s+[a-zA-Z0-9\-._~+\/]+=*/gi, REDACT_API_KEY_PATTERNS: [ /\b[a-zA-Z0-9]{32,}\b/g, // Generic long tokens /sk[-_]test[-_][a-zA-Z0-9]{24,}/gi, // Stripe test keys /sk[-_]live[-_][a-zA-Z0-9]{24,}/gi, // Stripe live keys /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, // UUIDs /ghp_[a-zA-Z0-9]{36}/g, // GitHub personal access tokens /gho_[a-zA-Z0-9]{36}/g // GitHub OAuth tokens ], REDACT_ENV_VAR_PATTERN: /\b[A-Z][A-Z0-9_]*_(KEY|SECRET|TOKEN|PASSWORD|API|CREDENTIAL)\b/g, REDACT_KEY_VALUE_PATTERN: /(api[_-]?key|secret|token|password|auth|bearer|credential|private[_-]?key)\s*[:=]\s*[^\s,;}]+/gi, // Validation MAX_SECRET_ID_LENGTH: 100, MAX_DOMAIN_LENGTH: 253, MAX_ACTION_LENGTH: 50, MAX_REASON_LENGTH: 500, MAX_URL_LENGTH: 2048, MAX_HEADER_NAME_LENGTH: 100, MAX_HEADER_VALUE_LENGTH: 8192, SECRET_ID_REGEX: /^[a-zA-Z0-9_-]+$/, ENV_VAR_REGEX: /^[A-Z][A-Z0-9_]*$/, MIN_SECRET_ID_LENGTH: 1, MIN_ENV_VAR_LENGTH: 1, MIN_DOMAIN_LENGTH: 3, DOMAIN_REGEX: /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i, ACTION_REGEX: /^[a-z_]+$/, URL_REGEX: /^https?:\/\/([\w\-]+\.)+[\w\-]+(:\d+)?(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?$/, HEADER_NAME_REGEX: /^[a-zA-Z0-9\-_]+$/, // File paths DEFAULT_CONFIG_FILE: "vault.config.json", ENV_VAULT_CONFIG: "VAULT_CONFIG", DEFAULT_POLICIES_DIR: "policies", DEFAULT_POLICIES_FILE: "policies.json", DEFAULT_MAPPINGS_FILE: "mappings.json", DEFAULT_AUDIT_DIR: "audit", // Log levels LOG_LEVEL_ERROR: "ERROR", LOG_LEVEL_WARN: "WARN", LOG_LEVEL_INFO: "INFO", LOG_LEVEL_DEBUG: "DEBUG", DEFAULT_LOG_LEVEL: "INFO", // Error codes ERROR_CODE_UNKNOWN_SECRET: "unknown_secret", ERROR_CODE_FORBIDDEN_DOMAIN: "forbidden_domain", ERROR_CODE_FORBIDDEN_ACTION: "forbidden_action", ERROR_CODE_TTL_EXPIRED: "ttl_expired", ERROR_CODE_RATE_LIMITED: "rate_limited", ERROR_CODE_PAYLOAD_TOO_LARGE: "payload_too_large", ERROR_CODE_MISSING_ENV: "missing_env", ERROR_CODE_TIMEOUT: "timeout", ERROR_CODE_INVALID_REQUEST: "invalid_request", ERROR_CODE_INVALID_POLICY: "invalid_policy", ERROR_CODE_POLICY_EXPIRED: "policy_expired", ERROR_CODE_NO_POLICY: "no_policy", ERROR_CODE_POLICIES_NOT_LOADED: "policies_not_loaded", ERROR_CODE_UNKNOWN_TOOL: "unknown_tool", ERROR_CODE_DUPLICATE_TOOL: "duplicate_tool", ERROR_CODE_INVALID_METHOD: "invalid_method", ERROR_CODE_INVALID_INJECTION_TYPE: "invalid_injection_type", ERROR_CODE_INVALID_URL: "invalid_url", ERROR_CODE_INVALID_HEADERS: "invalid_headers", ERROR_CODE_EXECUTION_FAILED: "execution_failed", // Pagination DEFAULT_PAGE_NUMBER: 1, DEFAULT_PAGE_SIZE: 50, // Function size limits (for code quality) MAX_FUNCTION_LINES: 20, // Cache settings CACHE_TTL_MS: 3e5, // 5 minutes // Header allowlist for responses ALLOWED_RESPONSE_HEADERS: [ "content-type", "content-length", "date", "etag", "cache-control", "x-request-id", "x-rate-limit-remaining", "x-rate-limit-reset" ], // Supported HTTP methods SUPPORTED_HTTP_METHODS: ["GET", "POST"], // Supported actions for policies SUPPORTED_ACTIONS: ["http_get", "http_post"], // HTTP header names HEADER_AUTHORIZATION: "authorization", HEADER_USER_AGENT: "user-agent", HEADER_CONTENT_TYPE: "content-type", // HTTP fetch options FETCH_REDIRECT_MODE: "manual", // Default values DEFAULT_TIMEZONE: "UTC", DEFAULT_ENCODING: "utf-8", // Security defaults (deny-by-default posture) DEFAULT_ALLOW_DOMAINS: [], DEFAULT_ALLOW_ACTIONS: [], DEFAULT_MAX_REQUEST_SIZE: 1024 * 1024, // 1MB DEFAULT_REQUIRE_HTTPS: true, DEFAULT_ALLOW_REDIRECTS: false, DEFAULT_SANITIZE_RESPONSES: true, DEFAULT_AUDIT_ALL_REQUESTS: true, // Environment variable prefix ENV_PREFIX: "MCP_VAULT_", // Exit codes EXIT_CODE_SUCCESS: 0, EXIT_CODE_ERROR: 1, EXIT_CODE_INVALID_CONFIG: 2, EXIT_CODE_MISSING_DEPENDENCY: 3, // File system error codes FS_ERROR_ENOENT: "ENOENT", // Unit conversion constants BYTES_PER_MB: 1024 * 1024, MS_PER_DAY: 24 * 60 * 60 * 1e3, // Numeric defaults ZERO_COUNT: 0, // Line ending pattern LINE_ENDING_PATTERN: /\r?\n/, // Process signals SIGNAL_INT: "SIGINT", SIGNAL_TERM: "SIGTERM", // Process arguments PROCESS_ARGV_FILE_INDEX: 1, // Environment variables ENV_TEST_SECRET_MAPPINGS: "TEST_SECRET_MAPPINGS", // URL schemes FILE_URL_SCHEME: "file://", INDEX_JS_SUFFIX: "/index.js", CLI_DOCTOR_MODULE: "./cli/doctor.js", // Logging patterns STACK_TRACE_PATTERN: "\n at ", EMPTY_STRING_FALLBACK: "", // Exception field names EXCEPTION_FIELD_NAMES: ["description", "environment"], // Sensitive key patterns for logging SENSITIVE_KEY_PATTERNS: ["SECRET", "KEY", "TOKEN", "PASSWORD"], // JSON Schema metadata JSON_SCHEMA_DRAFT: "http://json-schema.org/draft-07/schema#", JSON_SCHEMA_ID_PREFIX: "mcp-secrets-vault", JSON_SCHEMA_VERSION: "v1.0.0", JSON_SCHEMA_FILENAME: "vault.config.schema.json", JSON_SCHEMA_NAME: "VaultConfig", // ANSI color codes for terminal output ANSI_RESET: "\x1B[0m", ANSI_RED: "\x1B[31m", ANSI_GREEN: "\x1B[32m", ANSI_YELLOW: "\x1B[33m", ANSI_BLUE: "\x1B[34m", ANSI_CYAN: "\x1B[36m", ANSI_GRAY: "\x1B[90m", // CLI formatting CLI_SEPARATOR_LINE: "\u2550".repeat(60), CLI_SEPARATOR_LINE_THIN: "\u2501".repeat(60), // CLI status values CLI_STATUS_OK: "OK", CLI_STATUS_WARN: "WARN", CLI_STATUS_ERROR: "ERROR", // CLI arguments CLI_ARG_HELP_LONG: "--help", CLI_ARG_HELP_SHORT: "-h", // Content types CONTENT_TYPE_JSON: "application/json", // File encoding UTF8_ENCODING: "utf-8", // JSON formatting JSON_INDENT_SIZE: 2, // Zod schema configuration ZOD_REF_STRATEGY_NONE: "none", // Redaction patterns for logging REDACT_ENV_PATTERN: /\b[A-Z][A-Z0-9_]*_(PASSWORD|SECRET|KEY|TOKEN|CREDENTIAL|API)\b/g, REDACT_GENERIC_TOKEN: /\b[a-zA-Z0-9_-]{32,}\b/g, REDACT_CONTROL_CHARS: /[\x00-\x1F\x7F]/g, // Sensitive field names for logging SENSITIVE_FIELD_NAMES: [ "password", "secret", "token", "key", "auth", "authorization", "apikey", "api_key", "access_token", "refresh_token", "private_key", "client_secret", "webhook_secret", "signing_key", "encryption_key", "database_url", "connection_string", "env", "envvar", "environment" ], // Doctor CLI thresholds DOCTOR_RATE_LIMIT_MIN_REQUESTS: 10, DOCTOR_RATE_LIMIT_MAX_REQUESTS: 1e4, DOCTOR_RATE_LIMIT_MIN_WINDOW: 60, DOCTOR_RATE_LIMIT_MAX_WINDOW: 86400, DOCTOR_FILE_SIZE_WARN_MB: 500, DOCTOR_FILE_AGE_WARN_DAYS: 90, DOCTOR_POLICY_EXPIRING_SOON_DAYS: 7, DOCTOR_MIN_DOMAIN_COUNT: 1, DOCTOR_MAX_DOMAIN_COUNT: 100, DOCTOR_SUSPICIOUS_DOMAIN_PATTERNS: ["localhost", "127.0.0.1", "0.0.0.0", "test.com", "example.com"] }; // src/constants/text-constants.ts var TEXT = { // Error messages ERROR_UNKNOWN_SECRET: "Secret not found", ERROR_FORBIDDEN_DOMAIN: "Domain not allowed by policy", ERROR_FORBIDDEN_ACTION: "Action not allowed by policy", ERROR_TTL_EXPIRED: "Secret has expired", ERROR_RATE_LIMITED: "Rate limit exceeded", ERROR_PAYLOAD_TOO_LARGE: "Payload size exceeds limit", ERROR_MISSING_ENV: "Environment variable not set", ERROR_TIMEOUT: "Request timed out", ERROR_INVALID_REQUEST: "Invalid request format", ERROR_POLICY_NOT_FOUND: "Policy not found for secret", ERROR_POLICY_EXPIRED: "Policy has expired", ERROR_NO_MAPPING: "No mapping found for secret", ERROR_INVALID_ACTION: "Invalid action format", ERROR_INVALID_DOMAIN: "Invalid domain format", ERROR_INVALID_CONFIG: "Invalid configuration", ERROR_DUPLICATE_SECRET_ID: "Duplicate secret ID", ERROR_INVALID_SECRET_ID_FORMAT: "Invalid secret ID format", ERROR_INVALID_ENV_VAR_FORMAT: "Invalid environment variable format", ERROR_SECRET_ID_TOO_LONG: "Secret ID exceeds maximum length", ERROR_SECRET_ID_TOO_SHORT: "Secret ID is too short", ERROR_ENV_VAR_TOO_SHORT: "Environment variable name is too short", ERROR_INVALID_POLICY_STRUCTURE: "Invalid policy structure", ERROR_MISSING_POLICY_FIELD: "Required policy field missing", ERROR_INVALID_ALLOWED_ACTIONS: "Invalid allowed actions", ERROR_INVALID_ALLOWED_DOMAINS: "Invalid allowed domains", ERROR_INVALID_RATE_LIMIT: "Invalid rate limit configuration", ERROR_INVALID_EXPIRATION: "Invalid expiration date", ERROR_EMPTY_ALLOWED_ACTIONS: "Allowed actions cannot be empty", ERROR_EMPTY_ALLOWED_DOMAINS: "Allowed domains cannot be empty", ERROR_DUPLICATE_POLICY: "Duplicate policy for secret ID", ERROR_UNSUPPORTED_ACTION: "Unsupported action", ERROR_POLICIES_NOT_LOADED: "Policies not loaded", ERROR_NETWORK_ERROR: "Network request failed", ERROR_EMPTY_HEADER_NAME: "Header name cannot be empty", ERROR_UNKNOWN_TOOL: "Unknown tool requested", ERROR_TOOL_EXECUTION_FAILED: "Tool execution failed", ERROR_DUPLICATE_TOOL: "Duplicate tool registration", ERROR_INVALID_METHOD: "Invalid HTTP method", ERROR_INVALID_INJECTION_TYPE: "Invalid injection type", ERROR_INVALID_URL: "Invalid URL format", ERROR_INVALID_HEADERS: "Invalid headers format", ERROR_EXECUTION_FAILED: "Execution failed", ERROR_DOMAIN_TOO_SHORT: "Domain name is too short", ERROR_DOMAIN_TOO_LONG: "Domain name exceeds maximum length", ERROR_INVALID_DOMAIN_FORMAT: "Invalid domain format", ERROR_URL_TOO_LONG: "URL exceeds maximum length", ERROR_HTTPS_REQUIRED: "HTTPS is required for security", ERROR_INVALID_URL_FORMAT: "Invalid URL format", ERROR_ACTION_TOO_LONG: "Action exceeds maximum length", ERROR_INVALID_ACTION_FORMAT: "Invalid action format", ERROR_HEADER_NAME_TOO_LONG: "Header name exceeds maximum length", ERROR_INVALID_HEADER_NAME_FORMAT: "Invalid header name format", ERROR_HEADER_VALUE_TOO_LONG: "Header value exceeds maximum length", // Success messages SUCCESS_REQUEST_COMPLETED: "Request completed successfully", SUCCESS_POLICY_LOADED: "Policy loaded successfully", SUCCESS_AUDIT_WRITTEN: "Audit entry written", // Field names FIELD_SECRET_ID: "secretId", FIELD_ACTION: "action", FIELD_TYPE: "type", FIELD_URL: "url", FIELD_INJECTION_TYPE_LOWER: "injectionType", FIELD_DOMAIN: "domain", FIELD_TIMESTAMP: "timestamp", FIELD_OUTCOME: "outcome", FIELD_REASON: "reason", FIELD_ENV_VAR: "envVar", FIELD_ALLOWED_ACTIONS: "allowedActions", FIELD_ALLOWED_DOMAINS: "allowedDomains", FIELD_RATE_LIMIT: "rateLimit", FIELD_EXPIRES_AT: "expiresAt", FIELD_REQUESTS: "requests", FIELD_WINDOW_SECONDS: "windowSeconds", FIELD_METHOD: "method", FIELD_PAGE: "page", FIELD_PAGE_SIZE: "pageSize", FIELD_TOTAL_COUNT: "totalCount", FIELD_HAS_MORE: "hasMore", FIELD_START_TIME: "startTime", FIELD_END_TIME: "endTime", FIELD_SECRETS: "secrets", FIELD_AVAILABLE: "available", FIELD_DESCRIPTION: "description", FIELD_ENTRIES: "entries", FIELD_ERROR: "error", FIELD_CODE: "code", FIELD_MESSAGE: "message", FIELD_HEADER_NAME: "headerName", FIELD_HEADER_VALUE: "headerValue", // Log messages LOG_SERVER_STARTED: "MCP server started", LOG_SERVER_STOPPED: "MCP server stopped", LOG_SHUTDOWN_HANDLER_ERROR: "Shutdown handler error", LOG_PROCESSING_REQUEST: "Processing request", LOG_REQUEST_DENIED: "Request denied", LOG_REQUEST_ALLOWED: "Request allowed", LOG_LOADING_POLICIES: "Loading policies", LOG_LOADING_MAPPINGS: "Loading ENV mappings", // Audit outcomes AUDIT_OUTCOME_SUCCESS: "success", AUDIT_OUTCOME_DENIED: "denied", AUDIT_OUTCOME_ERROR: "error", // HTTP methods HTTP_METHOD_GET: "http_get", HTTP_METHOD_POST: "http_post", HTTP_VERB_GET: "GET", HTTP_VERB_POST: "POST", // Injection types INJECTION_TYPE_BEARER: "bearer", INJECTION_TYPE_HEADER: "header", // Header names AUTHORIZATION_HEADER: "Authorization", SECRET_HEADER_NAME: "X-Secret", // Response messages RESPONSE_NO_SECRETS: "No secrets configured", RESPONSE_POLICY_DESCRIPTION: "Policy for secret", RESPONSE_AUDIT_ENTRIES: "Audit entries", // Validation messages VALIDATION_REQUIRED_FIELD: "Required field missing", VALIDATION_INVALID_TYPE: "Invalid field type", VALIDATION_INVALID_FORMAT: "Invalid format", // Tool names TOOL_DISCOVER: "discover_secrets", TOOL_DESCRIBE: "describe_policy", TOOL_USE: "use_secret", TOOL_AUDIT: "query_audit", // Tool descriptions TOOL_DESC_DISCOVER: "List available secrets", TOOL_DESC_DESCRIBE: "Get policy details for a secret", TOOL_DESC_USE: "Use a secret to perform an action", TOOL_DESC_AUDIT: "Query audit log entries", TOOL_DISCOVER_DESCRIPTION: "List all available secret identifiers and their metadata", TOOL_AUDIT_DESCRIPTION: "Query audit log entries with filtering and pagination", // Input field descriptions INPUT_DESC_SECRET_ID: "The ID of the secret to describe policy for", INPUT_DESC_USE_SECRET_ID: "The ID of the secret to use", INPUT_DESC_ACTION: "The action to perform with the secret", INPUT_DESC_ACTION_TYPE: "The type of action to perform", INPUT_DESC_ACTION_URL: "The URL to make the request to", INPUT_DESC_ACTION_HEADERS: "Optional headers for the request", INPUT_DESC_ACTION_BODY: "Optional body for POST requests", INPUT_DESC_INJECTION_TYPE: "How to inject the secret (bearer or header)", // Schema types SCHEMA_TYPE_OBJECT: "object", SCHEMA_TYPE_STRING: "string", // Schema field names (JSON Schema standard keywords) SCHEMA_PROPERTIES: "properties", SCHEMA_REQUIRED: "required", SCHEMA_TYPE: "type", // Schema required arrays SCHEMA_REQUIRED_ACTION: ["type", "url"], SCHEMA_REQUIRED_USE_SECRET: ["secretId", "action"], // Field values FIELD_VALUE_UNKNOWN: "unknown", FIELD_VALUE_INVALID: "invalid", // Error message defaults ERROR_UNKNOWN: "Unknown error", // MCP Test messages TEST_MCP_HANDSHAKE: "Testing MCP protocol handshake", TEST_MCP_TOOL_DISCOVERY: "Testing MCP tool discovery", TEST_MCP_TOOL_EXECUTION: "Testing MCP tool execution", TEST_MCP_ERROR_HANDLING: "Testing MCP error handling", TEST_MCP_CONCURRENT_REQUESTS: "Testing MCP concurrent requests", TEST_MCP_PAYLOAD_SNAPSHOT: "Testing MCP payload snapshot", TEST_MCP_PROTOCOL_FLOW: "Testing MCP protocol flow", TEST_MCP_RATE_LIMITING: "Testing MCP rate limiting", TEST_MCP_AUDIT_TRAIL: "Testing MCP audit trail verification", // Test descriptions for snapshots SNAPSHOT_DESC_TOOL_METADATA: "Tool metadata snapshot", SNAPSHOT_DESC_SUCCESS_RESPONSE: "Successful response snapshot", SNAPSHOT_DESC_ERROR_RESPONSE: "Error response snapshot", SNAPSHOT_DESC_EMPTY_RESPONSE: "Empty response snapshot", SNAPSHOT_DESC_PAGINATION: "Pagination snapshot", SNAPSHOT_DESC_COMPLEX_OBJECT: "Complex object snapshot", // Schema generation messages SCHEMA_GENERATION_SUCCESS: "JSON Schema generated successfully", SCHEMA_GENERATION_FAILED: "Failed to generate JSON Schema", SCHEMA_REMINDER_EXACT_FQDN: "Remember: All domains must be exact FQDNs - wildcards are not allowed", SCHEMA_TITLE: "MCP Secrets Vault Configuration", SCHEMA_DESCRIPTION: "Configuration schema for MCP Secrets Vault. All domains must be exact FQDNs - wildcards are not allowed.", SCHEMA_DOMAIN_ITEM_DESC: 'Exact FQDN only (e.g., "api.example.com"). Wildcards (*, ?, []) are NOT allowed.', SCHEMA_DOMAIN_LIST_DESC: "List of exact FQDNs that can be accessed with this secret. No wildcards allowed.", // Doctor CLI messages DOCTOR_HEADER: "MCP Secrets Vault - System Diagnostics", DOCTOR_CHECKING_CONFIG: "Checking configuration...", DOCTOR_CONFIG_VALID: "Configuration is valid", DOCTOR_CONFIG_INVALID: "Configuration is invalid", DOCTOR_CONFIG_NOT_FOUND: "Configuration file not found", DOCTOR_CHECKING_ENV_VARS: "Checking environment variables...", DOCTOR_ENV_VAR_SET: "Environment variable is set", DOCTOR_ENV_VAR_NOT_SET: "Environment variable is not set", DOCTOR_CHECKING_DOMAINS: "Checking domain configurations...", DOCTOR_DOMAIN_VALID: "Domain configuration is valid", DOCTOR_DOMAIN_DUPLICATE: "Duplicate domain detected", DOCTOR_DOMAIN_SUSPICIOUS: "Suspicious domain pattern detected", DOCTOR_CHECKING_LIMITS: "Checking rate limits...", DOCTOR_LIMIT_REASONABLE: "Rate limit is reasonable", DOCTOR_LIMIT_TOO_HIGH: "Rate limit may be too high", DOCTOR_LIMIT_TOO_LOW: "Rate limit may be too low", DOCTOR_CHECK_PASSED: "All checks passed", DOCTOR_CHECK_WARNINGS: "Checks completed with warnings", DOCTOR_CHECK_ERRORS: "Checks completed with errors", DOCTOR_STATUS_OK: "OK", DOCTOR_STATUS_WARN: "WARN", DOCTOR_STATUS_ERROR: "ERROR", DOCTOR_SECRET_NOT_IN_ENV: "Secret ID mapped but ENV variable not set", DOCTOR_POLICY_WITHOUT_MAPPING: "Policy exists but no mapping found", DOCTOR_EXPIRED_POLICY: "Policy has already expired", DOCTOR_EXPIRING_SOON: "Policy expires soon", DOCTOR_NO_POLICIES: "No policies defined for mapped secrets", DOCTOR_AUDIT_DIR_NOT_WRITABLE: "Audit directory is not writable", DOCTOR_AUDIT_DIR_CREATED: "Audit directory created", DOCTOR_FILE_SIZE_WARNING: "Max file size may be too large", DOCTOR_FILE_AGE_WARNING: "Max file age may be too long", DOCTOR_SUMMARY_HEADER: "Diagnostic Summary", DOCTOR_TOTAL_CHECKS: "Total checks", DOCTOR_PASSED_CHECKS: "Passed", DOCTOR_WARNINGS: "Warnings", DOCTOR_ERRORS: "Errors", DOCTOR_RUNNING_CHECK: "Running check", DOCTOR_HELP_TEXT: "Use this tool to diagnose configuration and setup issues", // Index.ts DOCTOR_CLI_FAILED: "Doctor CLI failed", // Doctor check names DOCTOR_CHECK_CONFIG_SCHEMA: "Configuration Schema", DOCTOR_CHECK_ENV_VARS: "Environment Variables", DOCTOR_CHECK_DOMAINS: "Domain Configuration", DOCTOR_CHECK_RATE_LIMITS: "Rate Limits", DOCTOR_CHECK_AUDIT_DIR: "Audit Directory", DOCTOR_CHECK_POLICY_STATUS: "Policy Status", // Doctor messages (ALL literals must be replaced) DOCTOR_ENV_ALL_SET: "All {count} secrets have configured environment variables", DOCTOR_ENV_NONE_SET: "No environment variables are set", DOCTOR_DOMAIN_HAS_ISSUES: "Domain configuration has issues", DOCTOR_LIMITS_NEED_ADJUSTMENT: "Some rate limits may need adjustment", DOCTOR_AUDIT_WRITABLE_WITH_WARNINGS: "Audit directory is writable but has warnings", DOCTOR_AUDIT_WRITABLE: "Audit directory is writable: {path}", DOCTOR_POLICIES_ALL_VALID: "All policies are valid and properly mapped", DOCTOR_POLICIES_NEED_ATTENTION: "Policy configuration needs attention", DOCTOR_DOMAIN_COUNT_INFO: "Total unique domains: {count}", DOCTOR_LIMIT_WINDOW_SHORT: "Window too short ({seconds}s)", DOCTOR_LIMIT_WINDOW_LONG: "Window too long ({seconds}s)", DOCTOR_NO_DOMAINS_CONFIGURED: "{secretId}: No domains configured", DOCTOR_TOO_MANY_DOMAINS: "{secretId}: Too many domains ({count})", DOCTOR_ANALYZING: "Analyzing: {path}", DOCTOR_VERSION_INFO: "Version: {version}", DOCTOR_MAPPINGS_INFO: "Mappings: {count}", DOCTOR_POLICIES_INFO: "Policies: {count}", // Config validator messages CONFIG_VALIDATOR_VERSION_MUST_BE: "Must be {version} (current schema version)", CONFIG_VALIDATOR_WILDCARDS_NOT_ALLOWED: "Wildcards not allowed. Use exact FQDNs only (e.g., 'api.example.com')", CONFIG_VALIDATOR_VALIDATION_FAILED: "Configuration validation failed:", CONFIG_VALIDATOR_DUPLICATE_SECRET: "Duplicate secret ID found: {id}", CONFIG_VALIDATOR_DUPLICATE_POLICY: "Duplicate policy for secret ID: {id}", CONFIG_VALIDATOR_DOMAIN_MUST_BE_FQDN: "Domain must be an exact FQDN (no wildcards). Example: api.example.com", CONFIG_VALIDATOR_INVALID_ACTION: "Invalid action. Supported actions: {actions}", CONFIG_VALIDATOR_NOTE_DOMAINS: "Note: All domains must be exact FQDNs. Wildcards (*, ?, []) are not allowed.", CONFIG_VALIDATOR_ENV_VAR_FORMAT: "Environment variable must be uppercase with underscores", // Validate-config messages (if needed for consistency) VALIDATE_HEADER: "MCP Secrets Vault - Configuration Validator", VALIDATE_CHECKING_FILE: "Validating: {path}", VALIDATE_FILE_EXISTS: "File exists", VALIDATE_FILE_NOT_FOUND: "File not found", VALIDATE_LOADING: "Loading configuration...", VALIDATE_VALIDATING: "Validating structure...", VALIDATE_STRUCTURE_VALID: "Configuration structure is valid", // CLI status icons CLI_ICON_SUCCESS: "\u2705", CLI_ICON_WARNING: "\u26A0\uFE0F ", CLI_ICON_ERROR: "\u274C", // Index.ts messages INDEX_CONFIG_LOADED: "Configuration loaded: {mappings} mappings, {policies} policies", INDEX_CONFIG_LOAD_FAILED: "Failed to load configuration: {error}", INDEX_SERVER_START_FAILED: "Failed to start server", // CLI commands CLI_COMMAND_DOCTOR: "doctor", // Doctor CLI help text DOCTOR_HELP_HEADER: "MCP Secrets Vault - Doctor CLI", DOCTOR_HELP_DESCRIPTION: "Description:", DOCTOR_HELP_USAGE: "Usage:", DOCTOR_HELP_ARGUMENTS: "Arguments:", DOCTOR_HELP_EXAMPLES: "Examples:", DOCTOR_HELP_EXIT_CODES: "Exit Codes:", DOCTOR_HELP_CHECKS: "Checks Performed:", DOCTOR_HELP_CONFIG_ARG: "config-file Path to configuration file (default: vault.config.json)", DOCTOR_HELP_EXAMPLE_DEFAULT: "doctor # Check default vault.config.json", DOCTOR_HELP_EXAMPLE_CUSTOM: "doctor my-config.json # Check specific file", DOCTOR_HELP_EXAMPLE_HELP: "doctor --help # Show this help message", DOCTOR_HELP_EXIT_0: "0 All checks passed or only warnings", DOCTOR_HELP_EXIT_2: "2 Critical errors found", DOCTOR_HELP_CHECK_LIST: "\u2022 Configuration schema validity\n \u2022 Environment variable existence\n \u2022 Domain configuration coherence\n \u2022 Rate limit reasonableness\n \u2022 Audit directory accessibility\n \u2022 Policy expiration status", DOCTOR_FILE_NOT_FOUND: "File not found: {path}", DOCTOR_FATAL_ERROR: "Fatal error:", // Validate-config CLI messages VALIDATE_CONFIG_HEADER: "MCP Secrets Vault - Configuration Validator", VALIDATE_CONFIG_VALIDATING: "Validating:", VALIDATE_CONFIG_FILE_EXISTS: "File exists", VALIDATE_CONFIG_FILE_NOT_FOUND: "File not found", VALIDATE_CONFIG_CREATE_TIP: "Tip:", VALIDATE_CONFIG_CREATE_FILE_MSG: "Create a vault.config.json file with the following structure:", VALIDATE_CONFIG_LOADING: "Loading configuration...", VALIDATE_CONFIG_VALIDATING_STRUCTURE: "Validating structure...", VALIDATE_CONFIG_STRUCTURE_VALID: "Configuration structure is valid", VALIDATE_CONFIG_SUMMARY_HEADER: "Configuration Summary", VALIDATE_CONFIG_VERSION_LABEL: "Version", VALIDATE_CONFIG_MAPPINGS_LABEL: "Mappings", VALIDATE_CONFIG_POLICIES_LABEL: "Policies", VALIDATE_CONFIG_SECRET_MAPPINGS_HEADER: "Secret Mappings:", VALIDATE_CONFIG_ACCESS_POLICIES_HEADER: "Access Policies:", VALIDATE_CONFIG_ACTIONS_LABEL: "Actions:", VALIDATE_CONFIG_DOMAINS_LABEL: "Domains:", VALIDATE_CONFIG_EXACT_FQDNS_SUFFIX: "exact FQDNs", VALIDATE_CONFIG_RATE_LIMIT_FORMAT: "{requests} requests per {windowSeconds}s", VALIDATE_CONFIG_RATE_LIMIT_LABEL: "Rate Limit:", VALIDATE_CONFIG_EXPIRES_LABEL: "Expires:", VALIDATE_CONFIG_CHECKING_DUPLICATES: "Checking for duplicates...", VALIDATE_CONFIG_NO_DUPLICATES: "No duplicate secret IDs found", VALIDATE_CONFIG_SETTINGS_HEADER: "Settings:", VALIDATE_CONFIG_AUDIT_DIR_LABEL: "Audit Directory", VALIDATE_CONFIG_MAX_FILE_SIZE_LABEL: "Max File Size", VALIDATE_CONFIG_MAX_FILE_SIZE_SUFFIX: "MB", VALIDATE_CONFIG_MAX_FILE_AGE_LABEL: "Max File Age", VALIDATE_CONFIG_MAX_FILE_AGE_SUFFIX: "days", VALIDATE_CONFIG_DEFAULT_RATE_LIMIT_LABEL: "Default Rate Limit", VALIDATE_CONFIG_SECURITY_NOTES_HEADER: "Security Notes", VALIDATE_CONFIG_IMPORTANT_LABEL: "\u26A0\uFE0F Important:", VALIDATE_CONFIG_NOTE_EXACT_FQDNS: "\u2022 All domains must be exact FQDNs (no wildcards)", VALIDATE_CONFIG_NOTE_ENV_NOT_CHECKED: "\u2022 Environment variables are NOT checked by this validator", VALIDATE_CONFIG_NOTE_USE_DOCTOR: "\u2022 Use the doctor CLI (Task-17) to verify environment setup", VALIDATE_CONFIG_NOTE_DENY_BY_DEFAULT: "\u2022 Configuration follows deny-by-default security posture", VALIDATE_CONFIG_SUCCESS: "Configuration is valid!", VALIDATE_CONFIG_VALIDATION_FAILED: "Validation failed:", VALIDATE_CONFIG_WILDCARDS_TIP: "List each domain explicitly. For example:", VALIDATE_CONFIG_WILDCARDS_INSTEAD_OF: 'Instead of: "*.example.com"', VALIDATE_CONFIG_WILDCARDS_USE: 'Use: ["api.example.com", "www.example.com", "app.example.com"]', VALIDATE_CONFIG_JSON_TIP: "Check your JSON syntax with a JSON validator", VALIDATE_CONFIG_HELP_HEADER: "MCP Secrets Vault - Configuration Validator", VALIDATE_CONFIG_HELP_USAGE_LABEL: "Usage:", VALIDATE_CONFIG_HELP_USAGE: "validate-config [config-file]", VALIDATE_CONFIG_HELP_ARGUMENTS_LABEL: "Arguments:", VALIDATE_CONFIG_HELP_CONFIG_FILE: "config-file Path to configuration file (default: vault.config.json)", VALIDATE_CONFIG_HELP_EXAMPLES_LABEL: "Examples:", VALIDATE_CONFIG_HELP_EXAMPLE_DEFAULT: "validate-config # Validate default vault.config.json", VALIDATE_CONFIG_HELP_EXAMPLE_CUSTOM: "validate-config my-config.json # Validate specific file", VALIDATE_CONFIG_HELP_EXAMPLE_HELP: "validate-config --help # Show this help message", VALIDATE_CONFIG_HELP_EXIT_CODES_LABEL: "Exit Codes:", VALIDATE_CONFIG_HELP_EXIT_0: "0 Configuration is valid", VALIDATE_CONFIG_HELP_EXIT_2: "2 Configuration is invalid", VALIDATE_CONFIG_JSON_EXAMPLE: '{\n "version": "1.0.0",\n "mappings": [],\n "policies": [],\n "settings": {}\n}', VALIDATE_CONFIG_SECURITY_NOTE: "Security Notes:\n \u2022 Never commit actual secrets to version control\n \u2022 Use environment variables for sensitive values\n \u2022 Restrict file permissions appropriately", VALIDATE_CONFIG_HELP_TIP: "Tip: Run with DEBUG=* for detailed validation output", VALIDATE_CONFIG_JSON_ERROR: ": Invalid JSON in {path}", // Tool query audit descriptions TOOL_QUERY_AUDIT_SECRET_DESC: "Optional: Filter by secret ID", TOOL_QUERY_AUDIT_DOMAIN_DESC: "Optional: Filter by domain", TOOL_QUERY_AUDIT_OUTCOME_DESC: "Optional: Filter by outcome (success, denied, error)", TOOL_QUERY_AUDIT_START_DESC: "Optional: Start time (ISO 8601)", TOOL_QUERY_AUDIT_END_DESC: "Optional: End time (ISO 8601)", TOOL_QUERY_AUDIT_PAGE_DESC: "Optional: Page number (default: 1)", TOOL_QUERY_AUDIT_PAGE_SIZE_DESC: "Optional: Page size (default: 50, max: 500)", // Validation messages VALIDATION_NON_EMPTY_STRING: "{fieldName} must be a non-empty string" }; // src/utils/format.ts function fmt(template, params) { return template.replace( /{(\w+)}/g, (_, key) => params[key]?.toString() || `{${key}}` ); } // src/utils/security.ts function truncateText(text, maxLength) { if (text.length <= maxLength) { return text; } return text.substring(0, maxLength) + CONFIG.RESPONSE_TRUNCATION_MESSAGE; } function applyBasicRedaction(text) { return text.replace(CONFIG.REDACT_URL_AUTH_PATTERN, CONFIG.SANITIZE_REPLACEMENT).replace(CONFIG.REDACT_JWT_PATTERN, CONFIG.SANITIZE_REPLACEMENT).replace(CONFIG.REDACT_BEARER_TOKEN_PATTERN, CONFIG.SANITIZE_REPLACEMENT).replace(CONFIG.REDACT_ENV_VAR_PATTERN, CONFIG.SANITIZE_REPLACEMENT).replace(CONFIG.REDACT_KEY_VALUE_PATTERN, (_match, key) => `${key}=${CONFIG.SANITIZE_REPLACEMENT}`); } function applyApiKeyRedaction(text) { let result = text; for (const pattern of CONFIG.REDACT_API_KEY_PATTERNS) { result = result.replace(pattern, CONFIG.SANITIZE_REPLACEMENT); } return result; } function applyTokenRedaction(text) { return text.replace(/\b[a-zA-Z0-9_-]{32,}\b/g, (match) => { if (/^[a-zA-Z]+$/.test(match)) return match; if (/[a-zA-Z]/.test(match) && /[0-9_-]/.test(match)) { return CONFIG.SANITIZE_REPLACEMENT; } return match; }); } function redactSensitiveValue(value) { if (typeof value !== "string") { return String(value); } let redacted = applyBasicRedaction(value); redacted = applyApiKeyRedaction(redacted); redacted = applyTokenRedaction(redacted); return redacted; } function sanitizeForOutput(text, maxLength = CONFIG.RESPONSE_MAX_BODY_LENGTH) { const truncated = truncateText(text, maxLength); return redactSensitiveValue(truncated); } function sanitizeUrl(url) { try { const urlObj = new URL(url); urlObj.username = ""; urlObj.password = ""; const params = urlObj.searchParams; for (const key of Array.from(params.keys())) { if (CONFIG.SANITIZE_SECRET_PATTERN.test(key)) { params.set(key, CONFIG.SANITIZE_REPLACEMENT); } } return urlObj.toString(); } catch { return redactSensitiveValue(url); } } function isSensitiveKey(key) { const lowerKey = key.toLowerCase(); if (lowerKey === "secretid" || lowerKey === "secret_id" || lowerKey === "secrets") { return false; } return CONFIG.SENSITIVE_FIELD_NAMES.some( (sensitive) => lowerKey === sensitive || lowerKey.includes(sensitive) ); } function sanitizeObjectProperties(obj, maxDepth) { const sanitized = {}; for (const [key, value] of Object.entries(obj)) { if (isSensitiveKey(key)) { sanitized[key] = CONFIG.SANITIZE_REPLACEMENT; } else if (key === "url" && typeof value === "string") { sanitized[key] = sanitizeUrl(value); } else { sanitized[key] = deepSanitizeObject(value, maxDepth - 1); } } return sanitized; } function deepSanitizeObject(obj, maxDepth = 10) { if (maxDepth <= 0 || obj === null || obj === void 0) { return maxDepth <= 0 ? CONFIG.SANITIZE_REPLACEMENT : obj; } if (typeof obj === "string") { return redactSensitiveValue(obj); } if (typeof obj === "number" || typeof obj === "boolean") { return obj; } if (Array.isArray(obj)) { return obj.map((item) => deepSanitizeObject(item, maxDepth - 1)); } if (typeof obj === "object") { return sanitizeObjectProperties(obj, maxDepth); } return CONFIG.SANITIZE_REPLACEMENT; } function deepFreeze(obj) { if (obj === null || obj === void 0) { return obj; } if (typeof obj !== "object") { return obj; } Object.freeze(obj); Object.getOwnPropertyNames(obj).forEach((prop) => { const value = obj[prop]; if (value && typeof value === "object") { deepFreeze(value); } }); return obj; } function sanitizeError(error) { const err = error; if (err?.stack) { const firstLine = String(err.stack).split("\n")[0] || ""; return redactSensitiveValue(firstLine); } if (err?.message && typeof err.message === "string") { return redactSensitiveValue(err.message); } if (typeof error === "string") { return redactSensitiveValue(error); } return CONFIG.SANITIZE_REPLACEMENT; } function sanitizeHeaders(headers, allowedHeaders) { const sanitized = {}; const processHeader = (value, key) => { const normalizedKey = key.toLowerCase(); if (allowedHeaders.has(normalizedKey)) { sanitized[normalizedKey] = redactSensitiveValue(value); } }; const h = headers; if (h?.forEach && typeof h.forEach === "function") { h.forEach(processHeader); } else { Object.entries(headers).forEach(([key, value]) => { processHeader(value, key); }); } return sanitized; } function isEmptyOrWhitespace(value) { return !value || value.trim().length === 0; } function sanitizeResponse(response) { const sanitized = deepSanitizeObject(response); return deepFreeze(sanitized); } // src/utils/errors.ts var VaultError = class _VaultError extends Error { code; context; constructor(code, message, context) { const sanitizedMessage = typeof message === "string" ? message.replace(CONFIG.SANITIZE_SECRET_PATTERN, CONFIG.SANITIZE_REPLACEMENT) : String(message); super(sanitizedMessage); this.name = "VaultError"; this.code = code; if (context) { this.context = deepFreeze(deepSanitizeObject(context)); } Object.setPrototypeOf(this, _VaultError.prototype); } toJSON() { const json = { name: this.name, code: this.code, message: this.message, context: this.context }; return deepFreeze(json); } }; var ValidationError = class _ValidationError extends VaultError { constructor(code, message, field) { const context = field ? { field } : void 0; super(code, message, context); this.name = "ValidationError"; Object.setPrototypeOf(this, _ValidationError.prototype); } }; var ConfigurationError = class _ConfigurationError extends VaultError { constructor(message, field) { const context = field ? { field } : void 0; super(CONFIG.ERROR_CODE_INVALID_REQUEST, message, context); this.name = "ConfigurationError"; Object.setPrototypeOf(this, _ConfigurationError.prototype); } }; var ToolError = class _ToolError extends VaultError { constructor(message, code) { super(code, message); this.name = "ToolError"; Object.setPrototypeOf(this, _ToolError.prototype); } }; function sanitizeUnknownError(error) { if (error instanceof VaultError) { return error; } if (error instanceof Error) { return new VaultError( CONFIG.ERROR_CODE_EXECUTION_FAILED, TEXT.ERROR_EXECUTION_FAILED, { errorType: error.name } ); } return new VaultError( CONFIG.ERROR_CODE_EXECUTION_FAILED, TEXT.ERROR_EXECUTION_FAILED ); } // src/utils/validation.ts function isNonEmptyString(value) { return typeof value === "string" && value.trim().length > 0; } function validateSecretId(secretId) { const trimmed = secretId.trim(); if (trimmed.length < CONFIG.MIN_SECRET_ID_LENGTH) { throw new ValidationError( CONFIG.ERROR_CODE_INVALID_REQUEST, TEXT.ERROR_SECRET_ID_TOO_SHORT, TEXT.FIELD_SECRET_ID ); } if (trimmed.length > CONFIG.MAX_SECRET_ID_LENGTH) { throw new ValidationError( CONFIG.ERROR_CODE_INVALID_REQUEST, TEXT.ERROR_SECRET_ID_TOO_LONG, TEXT.FIELD_SECRET_ID ); } if (!CONFIG.SECRET_ID_REGEX.test(trimmed)) { throw new ValidationError( CONFIG.ERROR_CODE_INVALID_REQUEST, TEXT.ERROR_INVALID_SECRET_ID_FORMAT, TEXT.FIELD_SECRET_ID ); } return trimmed; } function validateEnvVar(envVar) { const trimmed = envVar.trim(); if (trimmed.length < CONFIG.MIN_ENV_VAR_LENGTH) { throw new ValidationError( CONFIG.ERROR_CODE_INVALID_REQUEST, TEXT.ERROR_ENV_VAR_TOO_SHORT, TEXT.FIELD_ENV_VAR ); } if (!CONFIG.ENV_VAR_REGEX.test(trimmed)) { throw new ValidationError( CONFIG.ERROR_CODE_INVALID_REQUEST, TEXT.ERROR_INVALID_ENV_VAR_FORMAT, TEXT.FIELD_ENV_VAR ); } return trimmed; } // src/services/env-secret-provider.ts var EnvSecretProvider = class { mappings; secretIds; constructor(mappings) { const validated = this.validateAndNormalizeMappings(mappings); this.mappings = Object.freeze(new Map(validated)); this.secretIds = Object.freeze(Array.from(this.mappings.keys()).sort()); } validateAndNormalizeMappings(mappings) { const seen = /* @__PURE__ */ new Set(); const result = []; for (const mapping of mappings) { const secretId = validateSecretId(mapping.secretId); const envVar = validateEnvVar(mapping.envVar); if (seen.has(secretId)) { throw new ConfigurationError( TEXT.ERROR_DUPLICATE_SECRET_ID, TEXT.FIELD_SECRET_ID ); } seen.add(secretId); const validated = Object.freeze({ secretId, envVar, description: mapping.description?.trim() }); result.push([secretId, validated]); } return result; } listSecretIds() { return this.secretIds; } isSecretAvailable(secretId) { const normalizedId = secretId.trim(); const mapping = this.mappings.get(normalizedId); if (!mapping) { return false; } const value = process.env[mapping.envVar]; return isNonEmptyString(value); } getSecretInfo(secretId) { const normalizedId = secretId.trim(); const mapping = this.mappings.get(normalizedId); if (!mapping) { return void 0; } return Object.freeze({ secretId: mapping.secretId, available: this.isSecretAvailable(normalizedId), description: mapping.description }); } getSecretValue(secretId) { const normalizedId = secretId.trim(); const mapping = this.mappings.get(normalizedId); if (!mapping) { return void 0; } const value = process.env[mapping.envVar]; return isNonEmptyString(value) ? value : void 0; } }; // src/services/policy-loader.service.ts import { promises as fs } from "fs"; import path from "path"; var PolicyLoaderService = class { constructor(policiesPath = path.join( CONFIG.DEFAULT_POLICIES_DIR, CONFIG.DEFAULT_POLICIES_FILE )) { this.policiesPath = policiesPath; } async loadPolicies() { try { const content = await fs.readFile(this.policiesPath, CONFIG.DEFAULT_ENCODING); const data = JSON.parse(content); if (!Array.isArray(data)) { throw new Error(TEXT.ERROR_INVALID_CONFIG); } return data.map((policy) => this.freezePolicy(policy)); } catch (error) { if (error.code === CONFIG.FS_ERROR_ENOENT) { return []; } if (error instanceof SyntaxError) { throw new Error(TEXT.ERROR_INVALID_CONFIG); } throw error; } } normalizeList(items) { if (!Array.isArray(items)) return Object.freeze([]); const normalized = /* @__PURE__ */ new Set(); for (const item of items) { const trimmed = item?.toString()?.trim()?.toLowerCase(); if (trimmed) normalized.add(trimmed); } return Object.freeze(Array.from(normalized).sort()); } freezePolicy(policy) { const frozen = { secretId: policy.secretId?.trim(), allowedActions: this.normalizeList(policy.allowedActions), allowedDomains: this.normalizeList(policy.allowedDomains) }; if (policy.rateLimit) { frozen.rateLimit = Object.freeze({ requests: policy.rateLimit.requests, windowSeconds: policy.rateLimit.windowSeconds }); } if (policy.expiresAt) frozen.expiresAt = policy.expiresAt?.trim(); return Object.freeze(frozen); } }; // src/services/policy-validator.service.ts import { z } from "zod"; var RateLimitSchema = z.object({ requests: z.number().positive(), windowSeconds: z.number().positive() }); var PolicyStructureSchema = z.object({}).passthrough(); var PolicyValidatorService = class { seenSecretIds = /* @__PURE__ */ new Set(); validate(policy) { this.validateStructure(policy); this.validateSecretId(policy.secretId); this.validateAllowedActions(policy.allowedActions); this.validateAllowedDomains(policy.allowedDomains); if (policy.rateLimit) { this.validateRateLimit(policy.rateLimit); } if (policy.expiresAt) { this.validateExpiration(policy.expiresAt); } } validateAll(policies) { this.seenSecretIds.clear(); for (const policy of policies) { this.validate(policy); const trimmedId = policy.secretId?.trim(); if (trimmedId && this.seenSecretIds.has(trimmedId)) { throw new ToolError(TEXT.ERROR_DUPLICATE_POLICY, CONFIG.ERROR_CODE_INVALID_POLICY); } if (trimmedId) { this.seenSecretIds.add(trimmedId); } } } validateStructure(policy) { try { PolicyStructureSchema.parse(policy); } catch { throw new ToolError(TEXT.ERROR_INVALID_POLICY_STRUCTURE, CONFIG.ERROR_CODE_INVALID_POLICY); } const requiredFields = [ TEXT.FIELD_SECRET_ID, TEXT.FIELD_ALLOWED_ACTIONS, TEXT.FIELD_ALLOWED_DOMAINS ]; for (const field of requiredFields) { if (!(field in policy)) { throw new ToolError( `${TEXT.ERROR_MISSING_POLICY_FIELD}: ${field}`, CONFIG.ERROR_CODE_INVALID_POLICY ); } } } validateSecretId(secretId) { const trimmed = secretId?.trim(); if (!trimmed) { throw new ToolError(TEXT.ERROR_SECRET_ID_TOO_SHORT, CONFIG.ERROR_CODE_INVALID_POLICY); } if (trimmed.length > CONFIG.MAX_SECRET_ID_LENGTH) { throw new ToolError(TEXT.ERROR_SECRET_ID_TOO_LONG, CONFIG.ERROR_CODE_INVALID_POLICY); } if (!CONFIG.SECRET_ID_REGEX.test(trimmed)) { throw new ToolError(TEXT.ERROR_INVALID_SECRET_ID_FORMAT, CONFIG.ERROR_CODE_INVALID_POLICY); } } validateAllowedActions(actions) { if (!Array.isArray(actions)) { throw new ToolError(TEXT.ERROR_INVALID_ALLOWED_ACTIONS, CONFIG.ERROR_CODE_INVALID_POLICY); } if (actions.length === 0) { throw new ToolError(TEXT.ERROR_EMPTY_ALLOWED_ACTIONS, CONFIG.ERROR_CODE_INVALID_POLICY); } for (const action of actions) { const trimmed = action?.trim(); if (!trimmed) { throw new ToolError(TEXT.ERROR_INVALID_ACTION, CONFIG.ERROR_CODE_INVALID_POLICY); } if (!CONFIG.ACTION_REGEX.test(trimmed)) { throw new ToolError(TEXT.ERROR_INVALID_ACTION, CONFIG.ERROR_CODE_INVALID_POLICY); } const supportedActions = CONFIG.SUPPORTED_ACTIONS; if (!supportedActions.includes(trimmed)) { throw new ToolError(`${TEXT.ERROR_UNSUPPORTED_ACTION}: ${trimmed}`, CONFIG.ERROR_CODE_INVALID_POLICY); } } } validateAllowedDomains(domains) { if (!Array.isArray(domains)) { throw new ToolError(TEXT.ERROR_INVALID_ALLOWED_DOMAINS, CONFIG.ERROR_CODE_INVALID_POLICY); } if (domains.length === 0) { throw new ToolError(TEXT.ERROR_EMPTY_ALLOWED_DOMAINS, CONFIG.ERROR_CODE_INVALID_POLICY); } for (const domain of domains) { const trimmed = domain?.trim(); if (!trimmed) { throw new ToolError(TEXT.ERROR_INVALID_DOMAIN, CONFIG.ERROR_CODE_INVALID_POLICY); } if (trimmed.length > CONFIG.MAX_DOMAIN_LENGTH) { throw new ToolError(TEXT.ERROR_INVALID_DOMAIN, CONFIG.ERROR_CODE_INVALID_POLICY); } if (trimmed.endsWith(".") || /\s/.test(trimmed)) { throw new ToolError(`${TEXT.ERROR_INVALID_DOMAIN}: ${trimmed}`, CONFIG.ERROR_CODE_INVALID_POLICY); } if (!CONFIG.DOMAIN_REGEX.test(trimmed)) { throw new ToolError(`${TEXT.ERROR_INVALID_DOMAIN}: ${trimmed}`, CONFIG.ERROR_CODE_INVALID_POLICY); } } } validateRateLimit(rateLimit) { try { RateLimitSchema.parse(rateLimit); } catch { throw new ToolError(TEXT.ERROR_INVALID_RATE_LIMIT, CONFIG.ERROR_CODE_INVALID_POLICY); } } validateExpiration(expiresAt) { const trimmed = expiresAt?.trim(); if (!trimmed) { throw new ToolError(TEXT.ERROR_INVALID_EXPIRATION, CONFIG.ERROR_CODE_INVALID_POLICY); } const date = new Date(trimmed); if (isNaN(date.getTime())) { throw new ToolError(TEXT.ERROR_INVALID_EXPIRATION, CONFIG.ERROR_CODE_INVALID_POLICY); } } }; // src/services/policy-evaluator.service.ts var PolicyEvaluatorService = class { policies = /* @__PURE__ */ new Map(); constructor(policies = []) { for (const policy of policies) { const trimmedKey = policy.secretId?.trim(); if (trimmedKey) { this.policies.set(trimmedKey, policy); } } } validateInput(secretId, action, domain) { const trimmedSecretId = secretId?.trim(); const trimmedDomain = domain?.trim()?.toLowerCase(); const normalizedAction = action?.trim()?.toLowerCase(); if (!trimmedSecretId || !normalizedAction || !trimmedDomain) { return { valid: false, error: { allowed: false, code: CONFIG.ERROR_CODE_INVALID_REQUEST, message: TEXT.ERROR_INVALID_REQUEST } }; } return { valid: true, trimmedSecretId, normalizedAction, trimmedDomain }; } validateAction(action) { const supportedActions = CONFIG.SUPPORTED_ACTIONS; if (!supportedActions.includes(action)) { return { allowed: false, code: CONFIG.ERROR_CODE_FORBIDDEN_ACTION, message: TEXT.ERROR_UNSUPPORTED_ACTION }; } return null; } validateExpiration(policy) { if (policy.expiresAt) { const expirationDate = new Date(policy.expiresAt); if (expirationDate <= /* @__PURE__ */ new Date()) { return { allowed: false, code: CONFIG.ERROR_CODE_POLICY_EXPIRED, message: TEXT.ERROR_POLICY_EXPIRED }; } } return null; } validatePolicyPermissions(policy, action, domain) { if (!policy.allowedActions.includes(action)) { return { allowed: false, code: CONFIG.ERROR_CODE_FORBIDDEN_ACTION, message: TEXT.ERROR_FORBIDDEN_ACTION }; } const domainAllowed = policy.allowedDomains.some( (allowedDomain) => allowedDomain.toLowerCase() === domain ); if (!domainAllowed) { return { allowed: false, code: CONFIG.ERROR_CODE_FORBIDDEN_DOMAIN, message: TEXT.ERROR_FORBIDDEN_DOMAIN }; } return { allowed: true }; } evaluate(secretId, action, domain) { const validation = this.validateInput(secretId, action, domain); if (!validation.valid) return validation.error; const actionError = this.validateAction(validation.normalizedAction); if (actionError) return actionError; const policy = this.policies.get(validation.trimmedSecretId); if (!policy) { return { allowed: false, code: CONFIG.ERROR_CODE_NO_POLICY, message: TEXT.ERROR_POLICY_NOT_FOUND }; } const expirationError = this.validateExpiration(policy); if (expirationError) return expirationError; return this.validatePolicyPermissions(policy, validation.normalizedAction, validation.trimmedDomain); } getPolicy(secretId) { const trimmed = secretId?.trim(); return trimmed ? this.policies.get(trimmed) : void 0; } hasPolicy(secretId) { const trimmed = secretId?.trim(); return trimmed ? this.policies.has(trimmed) : false; } }; // src/services/policy-provider.service.ts var PolicyProviderService = class { loader; validator; evaluator = null; constructor(policiesPath, loader, validator) { this.loader = loader || new PolicyLoaderService(policiesPath); this.validator = validator || new PolicyValidatorService(); } async loadPolicies() { const policies = await this.loader.loadPolicies(); this.validator.validateAll(policies); this.evaluator = new PolicyEvaluatorService(policies); } async loadPoliciesFromConfig(policies) { this.validator.validateAll(policies); this.evaluator = new PolicyEvaluatorService(policies); } evaluate(secretId, action, domain) { if (!this.evaluator) { return { allowed: false, code: CONFIG.ERROR_CODE_POLICIES_NOT_LOADED, message: TEXT.ERROR_POLICIES_NOT_LOADED }; } return this.evaluator.evaluate(secretId, action, domain); } getPolicy(secretId) { if (!this.evaluator) { return void 0; } return this.evaluator.getPolicy(secretId); } hasPolicy(secretId) { if (!this.evaluator) { return false; } return this.evaluator.hasPolicy(secretId); } }; // src/services/http-action-executor.service.ts var HttpActionExecutor = class { allowedHeaders; constructor() { this.allowedHeaders = new Set( CONFIG.ALLOWED_RESPONSE_HEADERS.map((h) => h.toLowerCase()) ); } /** * Execute an HTTP action with secret injection */ buildFetchOptions(request, headers, controller) { const options = { method: request.method, headers, signal: controller.signal, redirect: CONFIG.FETCH_REDIRECT_MODE }; if (request.method === "POST" && request.body) { options.body = JSON.stringify(request.body); headers[CONFIG.HEADER_CONTENT_TYPE] = CONFIG.CONTENT_TYPE_JSON; } return options; } handleExecutionError(error) { if (