/**
* Credential Manager
*
* Secure credential handling for Git MCP server with support for
* environment variables, validation, and provider-specific credentials.
*/
import { logger } from './logger.js';
export interface GitHubCredentials {
token: string;
username: string;
}
export interface GiteaCredentials {
url: string;
token: string;
username: string;
}
export interface CredentialValidationResult {
valid: boolean;
error?: string;
suggestions?: string[];
}
export interface ProviderCredentials {
github?: GitHubCredentials;
gitea?: GiteaCredentials;
}
export class CredentialManager {
private credentials: ProviderCredentials = {};
private validationCache: Map<string, { result: CredentialValidationResult; timestamp: number }> = new Map();
private cacheTimeout = 5 * 60 * 1000; // 5 minutes
constructor() {
this.loadCredentials();
}
/**
* Load credentials from environment variables
*/
loadCredentials(): void {
logger.debug('Loading credentials from environment variables', 'CREDENTIALS');
// Load GitHub credentials
const githubToken = process.env.GITHUB_TOKEN;
const githubUsername = process.env.GITHUB_USERNAME;
if (githubToken && githubUsername) {
this.credentials.github = {
token: githubToken,
username: githubUsername
};
logger.info('GitHub credentials loaded', 'CREDENTIALS', {
username: githubUsername,
tokenLength: githubToken.length
});
} else {
logger.debug('GitHub credentials not found in environment', 'CREDENTIALS', {
hasToken: !!githubToken,
hasUsername: !!githubUsername
});
}
// Load Gitea credentials
const giteaUrl = process.env.GITEA_URL;
const giteaToken = process.env.GITEA_TOKEN;
const giteaUsername = process.env.GITEA_USERNAME;
if (giteaUrl && giteaToken && giteaUsername) {
this.credentials.gitea = {
url: giteaUrl,
token: giteaToken,
username: giteaUsername
};
logger.info('Gitea credentials loaded', 'CREDENTIALS', {
url: giteaUrl,
username: giteaUsername,
tokenLength: giteaToken.length
});
} else {
logger.debug('Gitea credentials not found in environment', 'CREDENTIALS', {
hasUrl: !!giteaUrl,
hasToken: !!giteaToken,
hasUsername: !!giteaUsername
});
}
// Log overall credential status
const availableProviders = Object.keys(this.credentials);
logger.configuration('loaded', {
availableProviders,
providerCount: availableProviders.length
});
}
/**
* Get GitHub credentials
*/
getGitHubCredentials(): GitHubCredentials | undefined {
return this.credentials.github;
}
/**
* Get Gitea credentials
*/
getGiteaCredentials(): GiteaCredentials | undefined {
return this.credentials.gitea;
}
/**
* Get credentials for specific provider
*/
getProviderCredentials(provider: 'github' | 'gitea'): GitHubCredentials | GiteaCredentials | undefined {
return this.credentials[provider];
}
/**
* Check if provider is configured
*/
isProviderConfigured(provider: 'github' | 'gitea'): boolean {
return !!this.credentials[provider];
}
/**
* Get list of configured providers
*/
getConfiguredProviders(): ('github' | 'gitea')[] {
return Object.keys(this.credentials) as ('github' | 'gitea')[];
}
/**
* Validate provider credentials
*/
validateProviderCredentials(provider: 'github' | 'gitea'): CredentialValidationResult {
const cacheKey = `validate_${provider}`;
// Check cache first
const cached = this.validationCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.result;
}
let result: CredentialValidationResult;
if (provider === 'github') {
result = this.validateGitHubCredentials();
} else if (provider === 'gitea') {
result = this.validateGiteaCredentials();
} else {
result = {
valid: false,
error: `Unknown provider: ${provider}`,
suggestions: ['Use "github" or "gitea" as provider']
};
}
// Cache result
this.validationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
logger.debug(`Credential validation for ${provider}`, 'CREDENTIALS', {
provider,
valid: result.valid,
error: result.error
});
return result;
}
/**
* Validate GitHub credentials
*/
private validateGitHubCredentials(): CredentialValidationResult {
const creds = this.credentials.github;
if (!creds) {
return {
valid: false,
error: 'GitHub credentials not configured',
suggestions: [
'Set GITHUB_TOKEN environment variable',
'Set GITHUB_USERNAME environment variable',
'Ensure both variables are set correctly'
]
};
}
// Validate token format
if (!creds.token.startsWith('ghp_') && !creds.token.startsWith('github_pat_')) {
return {
valid: false,
error: 'Invalid GitHub token format',
suggestions: [
'GitHub personal access tokens should start with "ghp_" or "github_pat_"',
'Generate a new token at https://github.com/settings/tokens',
'Ensure the token has required permissions'
]
};
}
// Validate token length
if (creds.token.length < 20) {
return {
valid: false,
error: 'GitHub token appears to be too short',
suggestions: [
'Verify the complete token was copied',
'Generate a new token if the current one is incomplete'
]
};
}
// Validate username format
if (!creds.username || creds.username.trim().length === 0) {
return {
valid: false,
error: 'GitHub username is empty',
suggestions: [
'Set GITHUB_USERNAME environment variable',
'Use your GitHub username (not email)'
]
};
}
// Basic username validation
if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(creds.username)) {
return {
valid: false,
error: 'Invalid GitHub username format',
suggestions: [
'GitHub usernames can only contain alphanumeric characters and hyphens',
'Username cannot start or end with a hyphen',
'Verify your GitHub username is correct'
]
};
}
return { valid: true };
}
/**
* Validate Gitea credentials
*/
private validateGiteaCredentials(): CredentialValidationResult {
const creds = this.credentials.gitea;
if (!creds) {
return {
valid: false,
error: 'Gitea credentials not configured',
suggestions: [
'Set GITEA_URL environment variable',
'Set GITEA_TOKEN environment variable',
'Set GITEA_USERNAME environment variable',
'Ensure all three variables are set correctly'
]
};
}
// Validate URL format
try {
const url = new URL(creds.url);
if (!['http:', 'https:'].includes(url.protocol)) {
return {
valid: false,
error: 'Gitea URL must use HTTP or HTTPS protocol',
suggestions: [
'Use https:// or http:// in GITEA_URL',
'Example: https://gitea.example.com'
]
};
}
} catch {
return {
valid: false,
error: 'Invalid Gitea URL format',
suggestions: [
'Provide a valid URL in GITEA_URL',
'Example: https://gitea.example.com',
'Include protocol (https:// or http://)'
]
};
}
// Validate token
if (!creds.token || creds.token.trim().length === 0) {
return {
valid: false,
error: 'Gitea token is empty',
suggestions: [
'Set GITEA_TOKEN environment variable',
'Generate a token in your Gitea instance settings'
]
};
}
// Validate token length (Gitea tokens are typically 40 characters)
if (creds.token.length < 20) {
return {
valid: false,
error: 'Gitea token appears to be too short',
suggestions: [
'Verify the complete token was copied',
'Generate a new token if the current one is incomplete'
]
};
}
// Validate username
if (!creds.username || creds.username.trim().length === 0) {
return {
valid: false,
error: 'Gitea username is empty',
suggestions: [
'Set GITEA_USERNAME environment variable',
'Use your Gitea username (not email)'
]
};
}
return { valid: true };
}
/**
* Validate all configured credentials
*/
validateAllCredentials(): Record<string, CredentialValidationResult> {
const results: Record<string, CredentialValidationResult> = {};
for (const provider of this.getConfiguredProviders()) {
results[provider] = this.validateProviderCredentials(provider);
}
return results;
}
/**
* Get credential configuration status
*/
getCredentialStatus(): {
configured: string[];
missing: string[];
valid: string[];
invalid: string[];
errors: Record<string, string>;
} {
const allProviders = ['github', 'gitea'] as const;
const configured: string[] = [];
const missing: string[] = [];
const valid: string[] = [];
const invalid: string[] = [];
const errors: Record<string, string> = {};
for (const provider of allProviders) {
if (this.isProviderConfigured(provider)) {
configured.push(provider);
const validation = this.validateProviderCredentials(provider);
if (validation.valid) {
valid.push(provider);
} else {
invalid.push(provider);
errors[provider] = validation.error || 'Unknown validation error';
}
} else {
missing.push(provider);
}
}
return {
configured,
missing,
valid,
invalid,
errors
};
}
/**
* Get configuration guide for missing providers
*/
getConfigurationGuide(providers?: ('github' | 'gitea')[]): Record<string, {
envVars: string[];
example: Record<string, string>;
instructions: string[];
}> {
const targetProviders = providers || ['github', 'gitea'];
const guide: Record<string, any> = {};
for (const provider of targetProviders) {
if (provider === 'github') {
guide.github = {
envVars: ['GITHUB_TOKEN', 'GITHUB_USERNAME'],
example: {
GITHUB_TOKEN: 'ghp_xxxxxxxxxxxxxxxxxxxx',
GITHUB_USERNAME: 'your-github-username'
},
instructions: [
'1. Go to https://github.com/settings/tokens',
'2. Click "Generate new token (classic)"',
'3. Select required scopes (repo, user)',
'4. Copy the generated token',
'5. Set GITHUB_TOKEN environment variable',
'6. Set GITHUB_USERNAME to your GitHub username'
]
};
}
if (provider === 'gitea') {
guide.gitea = {
envVars: ['GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME'],
example: {
GITEA_URL: 'https://gitea.example.com',
GITEA_TOKEN: 'your-gitea-access-token',
GITEA_USERNAME: 'your-gitea-username'
},
instructions: [
'1. Log in to your Gitea instance',
'2. Go to Settings > Applications',
'3. Generate new access token',
'4. Copy the generated token',
'5. Set GITEA_URL to your Gitea instance URL',
'6. Set GITEA_TOKEN to the generated token',
'7. Set GITEA_USERNAME to your Gitea username'
]
};
}
}
return guide;
}
/**
* Mask sensitive credential information for logging
*/
maskCredentials(credentials: any): any {
if (!credentials || typeof credentials !== 'object') {
return credentials;
}
const masked = { ...credentials };
// Mask tokens
if (masked.token && typeof masked.token === 'string') {
masked.token = this.maskToken(masked.token);
}
// Recursively mask nested objects
for (const key in masked) {
if (typeof masked[key] === 'object' && masked[key] !== null) {
masked[key] = this.maskCredentials(masked[key]);
}
}
return masked;
}
/**
* Mask token for safe logging
*/
private maskToken(token: string): string {
if (token.length <= 8) {
return '*'.repeat(token.length);
}
const start = token.substring(0, 4);
const end = token.substring(token.length - 4);
const middle = '*'.repeat(token.length - 8);
return `${start}${middle}${end}`;
}
/**
* Clear validation cache
*/
clearValidationCache(): void {
this.validationCache.clear();
logger.debug('Credential validation cache cleared', 'CREDENTIALS');
}
/**
* Refresh credentials from environment
*/
refreshCredentials(): void {
this.clearValidationCache();
this.loadCredentials();
logger.info('Credentials refreshed from environment', 'CREDENTIALS');
}
/**
* Get credential summary for status reporting
*/
getCredentialSummary(): {
totalProviders: number;
configuredProviders: number;
validProviders: number;
providers: Record<string, {
configured: boolean;
valid: boolean;
error?: string;
}>;
} {
const status = this.getCredentialStatus();
const providers: Record<string, any> = {};
// Add configured providers
for (const provider of status.configured) {
const isValid = status.valid.includes(provider);
providers[provider] = {
configured: true,
valid: isValid,
error: isValid ? undefined : status.errors[provider]
};
}
// Add missing providers
for (const provider of status.missing) {
providers[provider] = {
configured: false,
valid: false,
error: 'Not configured'
};
}
return {
totalProviders: 2, // github + gitea
configuredProviders: status.configured.length,
validProviders: status.valid.length,
providers
};
}
}
// Export singleton instance
export const credentialManager = new CredentialManager();