forked from claude-did-this/claude-hub
* feat: Implement Claude orchestration with session management - Add CLAUDE_WEBHOOK_SECRET for webhook authentication - Fix Docker volume mounting for Claude credentials - Capture Claude's internal session ID from stream-json output - Update entrypoint script to support OUTPUT_FORMAT=stream-json - Fix environment variable naming (REPOSITORY -> REPO_FULL_NAME) - Enable parallel session execution with proper authentication - Successfully tested creating PRs via orchestrated sessions This enables the webhook to create and manage Claude Code sessions that can: - Clone repositories - Create feature branches - Implement code changes - Commit and push changes - Create pull requests All while capturing Claude's internal session ID for potential resumption. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Update SessionManager tests for new implementation - Update test to expect docker volume create instead of docker create - Add unref() method to mock process objects to fix test environment error - Update spawn expectations to match new docker run implementation - Fix tests for both startSession and queueSession methods Tests now pass in CI environment. --------- Co-authored-by: Claude <noreply@anthropic.com>
140 lines
3.5 KiB
TypeScript
140 lines
3.5 KiB
TypeScript
import fs from 'fs';
|
|
import { logger } from './logger';
|
|
|
|
interface CredentialConfig {
|
|
file: string;
|
|
env: string;
|
|
}
|
|
|
|
interface CredentialMappings {
|
|
[key: string]: CredentialConfig;
|
|
}
|
|
|
|
/**
|
|
* Secure credential loader - reads from files instead of env vars
|
|
* Files are mounted as Docker secrets or regular files
|
|
*/
|
|
class SecureCredentials {
|
|
private credentials: Map<string, string>;
|
|
|
|
constructor() {
|
|
this.credentials = new Map();
|
|
this.loadCredentials();
|
|
}
|
|
|
|
/**
|
|
* Load credentials from files or fallback to env vars
|
|
*/
|
|
private loadCredentials(): void {
|
|
const credentialMappings: CredentialMappings = {
|
|
GITHUB_TOKEN: {
|
|
file: process.env['GITHUB_TOKEN_FILE'] ?? '/run/secrets/github_token',
|
|
env: 'GITHUB_TOKEN'
|
|
},
|
|
ANTHROPIC_API_KEY: {
|
|
file: process.env['ANTHROPIC_API_KEY_FILE'] ?? '/run/secrets/anthropic_api_key',
|
|
env: 'ANTHROPIC_API_KEY'
|
|
},
|
|
GITHUB_WEBHOOK_SECRET: {
|
|
file: process.env['GITHUB_WEBHOOK_SECRET_FILE'] ?? '/run/secrets/webhook_secret',
|
|
env: 'GITHUB_WEBHOOK_SECRET'
|
|
},
|
|
CLAUDE_WEBHOOK_SECRET: {
|
|
file: process.env['CLAUDE_WEBHOOK_SECRET_FILE'] ?? '/run/secrets/claude_webhook_secret',
|
|
env: 'CLAUDE_WEBHOOK_SECRET'
|
|
}
|
|
};
|
|
|
|
for (const [key, config] of Object.entries(credentialMappings)) {
|
|
let value: string | null = null;
|
|
|
|
// Try to read from file first (most secure)
|
|
try {
|
|
// eslint-disable-next-line no-sync
|
|
if (fs.existsSync(config.file)) {
|
|
// eslint-disable-next-line no-sync
|
|
value = fs.readFileSync(config.file, 'utf8').trim();
|
|
logger.info(`Loaded ${key} from secure file: ${config.file}`);
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
logger.warn(`Failed to read ${key} from file ${config.file}: ${errorMessage}`);
|
|
}
|
|
|
|
// Fallback to environment variable (less secure)
|
|
if (!value && process.env[config.env]) {
|
|
value = process.env[config.env] as string;
|
|
logger.warn(`Using ${key} from environment variable (less secure)`);
|
|
}
|
|
|
|
if (value) {
|
|
this.credentials.set(key, value);
|
|
} else {
|
|
logger.error(`No credential found for ${key}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get credential value
|
|
*/
|
|
get(key: string): string | null {
|
|
return this.credentials.get(key) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Check if credential exists
|
|
*/
|
|
has(key: string): boolean {
|
|
return this.credentials.has(key);
|
|
}
|
|
|
|
/**
|
|
* Get all available credential keys (for debugging)
|
|
*/
|
|
getAvailableKeys(): string[] {
|
|
return Array.from(this.credentials.keys());
|
|
}
|
|
|
|
/**
|
|
* Reload credentials (useful for credential rotation)
|
|
*/
|
|
reload(): void {
|
|
this.credentials.clear();
|
|
this.loadCredentials();
|
|
logger.info('Credentials reloaded');
|
|
}
|
|
|
|
/**
|
|
* Add or update a credential programmatically
|
|
*/
|
|
set(key: string, value: string): void {
|
|
this.credentials.set(key, value);
|
|
logger.debug(`Credential ${key} updated programmatically`);
|
|
}
|
|
|
|
/**
|
|
* Remove a credential
|
|
*/
|
|
delete(key: string): boolean {
|
|
const deleted = this.credentials.delete(key);
|
|
if (deleted) {
|
|
logger.debug(`Credential ${key} removed`);
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
/**
|
|
* Get credential count
|
|
*/
|
|
size(): number {
|
|
return this.credentials.size;
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const secureCredentials = new SecureCredentials();
|
|
|
|
export default secureCredentials;
|
|
export { SecureCredentials };
|