Compare commits

...

3 Commits

Author SHA1 Message Date
Jonathan
871762add6 Merge remote-tracking branch 'origin/main' into remove-n8n-network-dependency 2025-05-30 10:16:35 -05:00
Jonathan
91b4cad93d refactor: simplify secrets management to use .env file
- Remove complex file-based secrets system and secureCredentials.ts
- Switch to standard .env file approach for all credentials
- Update all code to use process.env directly instead of secureCredentials
- Remove docker-compose.secrets.yml and setup-secure-credentials.sh
- Update tests to use environment variables directly
- Simplify Docker Compose configuration to use env vars

This change reduces complexity while maintaining the same security level,
as both approaches store plaintext credentials on the host. The .env
approach is simpler, more standard, and easier to manage.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-30 08:56:42 -05:00
Jonathan
e17313ac43 refactor: remove n8n network dependency
Replace external n8n_default network with dedicated claude-hub bridge network
to eliminate external dependencies and improve deployment flexibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-30 08:36:59 -05:00
17 changed files with 18 additions and 364 deletions

View File

@@ -1,36 +0,0 @@
version: '3.8'
services:
webhook:
build: .
ports:
- "3003:3002"
secrets:
- github_token
- anthropic_api_key
- webhook_secret
environment:
- NODE_ENV=production
- PORT=3002
- AUTHORIZED_USERS=Cheffromspace
- BOT_USERNAME=@MCPClaude
- DEFAULT_GITHUB_OWNER=Cheffromspace
- DEFAULT_GITHUB_USER=Cheffromspace
- DEFAULT_BRANCH=main
- CLAUDE_USE_CONTAINERS=1
- CLAUDE_CONTAINER_IMAGE=claudecode:latest
# Point to secret files instead of env vars
- GITHUB_TOKEN_FILE=/run/secrets/github_token
- ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
- GITHUB_WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
secrets:
github_token:
file: ./secrets/github_token.txt
anthropic_api_key:
file: ./secrets/anthropic_api_key.txt
webhook_secret:
file: ./secrets/webhook_secret.txt

View File

@@ -9,10 +9,6 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ${HOME}/.aws:/root/.aws:ro
- ${HOME}/.claude:/home/claudeuser/.claude
secrets:
- github_token
- anthropic_api_key
- webhook_secret
environment:
- NODE_ENV=production
- PORT=3002
@@ -29,10 +25,10 @@ services:
- PR_REVIEW_DEBOUNCE_MS=${PR_REVIEW_DEBOUNCE_MS:-5000}
- PR_REVIEW_MAX_WAIT_MS=${PR_REVIEW_MAX_WAIT_MS:-1800000}
- PR_REVIEW_CONDITIONAL_TIMEOUT_MS=${PR_REVIEW_CONDITIONAL_TIMEOUT_MS:-300000}
# Point to secret files instead of env vars
- GITHUB_TOKEN_FILE=/run/secrets/github_token
- ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
- GITHUB_WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
# Secrets from environment variables
- GITHUB_TOKEN=${GITHUB_TOKEN}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
@@ -41,16 +37,8 @@ services:
retries: 3
start_period: 10s
networks:
- n8n_default
secrets:
github_token:
file: ./secrets/github_token.txt
anthropic_api_key:
file: ./secrets/anthropic_api_key.txt
webhook_secret:
file: ./secrets/webhook_secret.txt
- claude-hub
networks:
n8n_default:
external: true
claude-hub:
driver: bridge

View File

@@ -1,92 +0,0 @@
#!/bin/bash
# Setup Secure Credentials Script
# Creates secure credential files with proper permissions
set -e
echo "🔐 Setting up secure credentials..."
# Create secrets directory
SECRETS_DIR="./secrets"
mkdir -p "$SECRETS_DIR"
# Set restrictive permissions on secrets directory
chmod 700 "$SECRETS_DIR"
echo "📁 Created secrets directory: $SECRETS_DIR"
# Function to create secure credential file
create_credential_file() {
local filename="$1"
local description="$2"
local filepath="$SECRETS_DIR/$filename"
if [ -f "$filepath" ]; then
echo "⚠️ $filepath already exists, skipping..."
return
fi
echo "🔑 Creating $description credential file..."
read -s -p "Enter $description: " credential
echo
# Write credential to file
echo "$credential" > "$filepath"
# Set secure permissions (owner read-only)
chmod 600 "$filepath"
echo "✅ Created $filepath with secure permissions"
}
# Create credential files
create_credential_file "github_token.txt" "GitHub Personal Access Token"
create_credential_file "anthropic_api_key.txt" "Anthropic API Key"
create_credential_file "webhook_secret.txt" "GitHub Webhook Secret"
# Create .env file without secrets
cat > .env.secure << 'EOF'
# Secure Configuration (no secrets in env vars)
NODE_ENV=production
PORT=3002
# Bot Configuration
BOT_USERNAME=@MCPClaude
DEFAULT_GITHUB_OWNER=Cheffromspace
DEFAULT_GITHUB_USER=Cheffromspace
DEFAULT_BRANCH=main
# Security Configuration
AUTHORIZED_USERS=Cheffromspace
# Container Configuration
CLAUDE_USE_CONTAINERS=1
CLAUDE_CONTAINER_IMAGE=claudecode:latest
# Credential file paths (Docker secrets)
GITHUB_TOKEN_FILE=/run/secrets/github_token
ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
GITHUB_WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
EOF
echo "✅ Created .env.secure configuration file"
# Update .gitignore to exclude secrets
if ! grep -q "secrets/" .gitignore 2>/dev/null; then
echo "secrets/" >> .gitignore
echo "✅ Added secrets/ to .gitignore"
fi
echo ""
echo "🎉 Secure credentials setup complete!"
echo ""
echo "Next steps:"
echo "1. Start with Docker secrets: docker compose -f docker-compose.secrets.yml up -d"
echo "2. Or use local files: cp .env.secure .env && npm start"
echo "3. Verify credentials are loaded: check application logs"
echo ""
echo "🔒 Security notes:"
echo "- Credential files have 600 permissions (owner read-only)"
echo "- secrets/ directory is added to .gitignore"
echo "- Use Docker secrets in production for maximum security"

View File

@@ -10,7 +10,6 @@ import {
} from '../services/githubService';
import { createLogger } from '../utils/logger';
import { sanitizeBotMentions } from '../utils/sanitize';
import secureCredentials from '../utils/secureCredentials';
import type {
WebhookHandler,
WebhookRequest,
@@ -68,9 +67,9 @@ function verifyWebhookSignature(req: WebhookRequest): boolean {
'Verifying webhook signature'
);
const webhookSecret = secureCredentials.get('GITHUB_WEBHOOK_SECRET');
const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET;
if (!webhookSecret) {
logger.error('GITHUB_WEBHOOK_SECRET not found in secure credentials');
logger.error('GITHUB_WEBHOOK_SECRET not found in environment');
throw new Error('Webhook secret not configured');
}

View File

@@ -2,7 +2,6 @@ const { verify } = require('crypto');
const axios = require('axios');
const ChatbotProvider = require('./ChatbotProvider');
const { createLogger } = require('../utils/logger');
const secureCredentials = require('../utils/secureCredentials');
const logger = createLogger('DiscordProvider');
@@ -23,11 +22,9 @@ class DiscordProvider extends ChatbotProvider {
*/
async initialize() {
try {
this.botToken = secureCredentials.get('DISCORD_BOT_TOKEN') || process.env.DISCORD_BOT_TOKEN;
this.publicKey =
secureCredentials.get('DISCORD_PUBLIC_KEY') || process.env.DISCORD_PUBLIC_KEY;
this.applicationId =
secureCredentials.get('DISCORD_APPLICATION_ID') || process.env.DISCORD_APPLICATION_ID;
this.botToken = process.env.DISCORD_BOT_TOKEN;
this.publicKey = process.env.DISCORD_PUBLIC_KEY;
this.applicationId = process.env.DISCORD_APPLICATION_ID;
if (!this.botToken || !this.publicKey) {
throw new Error('Discord bot token and public key are required');

View File

@@ -4,7 +4,6 @@ import { execFile } from 'child_process';
import path from 'path';
import { createLogger } from '../utils/logger';
import { sanitizeBotMentions } from '../utils/sanitize';
import secureCredentials from '../utils/secureCredentials';
import type {
ClaudeCommandOptions,
OperationType,
@@ -52,7 +51,7 @@ export async function processCommand({
'Processing command with Claude'
);
const githubToken = secureCredentials.get('GITHUB_TOKEN');
const githubToken = process.env.GITHUB_TOKEN;
// In test mode, skip execution and return a mock response
if (process.env['NODE_ENV'] === 'test' || !githubToken?.includes('ghp_')) {
@@ -351,7 +350,7 @@ function createEnvironmentVars({
OPERATION_TYPE: operationType,
COMMAND: fullPrompt,
GITHUB_TOKEN: githubToken,
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY') ?? ''
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? ''
};
}
@@ -508,7 +507,7 @@ function handleDockerExecutionError(
// Sensitive values to redact
const sensitiveValues = [
context.githubToken,
secureCredentials.get('ANTHROPIC_API_KEY')
process.env.ANTHROPIC_API_KEY
].filter(val => val && val.length > 0);
// Redact specific sensitive values first

View File

@@ -1,6 +1,5 @@
import { Octokit } from '@octokit/rest';
import { createLogger } from '../utils/logger';
import secureCredentials from '../utils/secureCredentials';
import type {
CreateCommentRequest,
CreateCommentResponse,
@@ -23,7 +22,7 @@ let octokit: Octokit | null = null;
function getOctokit(): Octokit | null {
if (!octokit) {
const githubToken = secureCredentials.get('GITHUB_TOKEN');
const githubToken = process.env.GITHUB_TOKEN;
if (githubToken?.includes('ghp_')) {
octokit = new Octokit({
auth: githubToken,

View File

@@ -1,135 +0,0 @@
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'
}
};
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 };

View File

@@ -10,10 +10,6 @@ jest.mock('../../../src/utils/logger', () => ({
})
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn(),
loadCredentials: jest.fn()
}));
// Set required environment variables for claudeService
process.env.BOT_USERNAME = 'testbot';

View File

@@ -4,28 +4,9 @@ const SignatureHelper = require('../../utils/signatureHelper');
process.env.BOT_USERNAME = '@TestBot';
process.env.NODE_ENV = 'test';
process.env.GITHUB_TOKEN = 'test_token';
process.env.GITHUB_WEBHOOK_SECRET = 'test_webhook_secret';
process.env.AUTHORIZED_USERS = 'testuser,admin';
// Mock secureCredentials before requiring actual modules
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn(key => {
const mockCredentials = {
GITHUB_WEBHOOK_SECRET: 'test_secret',
GITHUB_TOKEN: 'test_token',
ANTHROPIC_API_KEY: 'test_anthropic_key'
};
return mockCredentials[key] || null;
}),
has: jest.fn(key => {
const mockCredentials = {
GITHUB_WEBHOOK_SECRET: 'test_secret',
GITHUB_TOKEN: 'test_token',
ANTHROPIC_API_KEY: 'test_anthropic_key'
};
return !!mockCredentials[key];
})
}));
// Mock services before requiring actual modules
jest.mock('../../../src/services/claudeService', () => ({
processCommand: jest.fn().mockResolvedValue('Claude response')

View File

@@ -12,9 +12,6 @@ jest.mock('../../../src/utils/logger', () => ({
})
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn()
}));
const mockSecureCredentials = require('../../../src/utils/secureCredentials');

View File

@@ -8,10 +8,6 @@ jest.mock('../../../src/utils/logger', () => ({
})
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn(),
loadCredentials: jest.fn()
}));
const _ProviderFactory = require('../../../src/providers/ProviderFactory');
const DiscordProvider = require('../../../src/providers/DiscordProvider');

View File

@@ -10,9 +10,6 @@ jest.mock('../../../src/utils/logger', () => ({
})
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn().mockReturnValue('mock_value')
}));
describe('Discord Payload Processing Tests', () => {
let provider;

View File

@@ -11,9 +11,6 @@ jest.mock('../../../src/utils/logger', () => ({
})
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn()
}));
const mockSecureCredentials = require('../../../src/utils/secureCredentials');

View File

@@ -2,6 +2,7 @@
process.env.BOT_USERNAME = '@TestBot';
process.env.NODE_ENV = 'test';
process.env.GITHUB_TOKEN = 'ghp_test_token'; // Use token format that passes validation
process.env.ANTHROPIC_API_KEY = 'sk-ant-test-key';
// Mock dependencies
jest.mock('child_process', () => ({
@@ -40,14 +41,6 @@ jest.mock('../../../src/utils/sanitize', () => ({
sanitizeBotMentions: jest.fn(input => input)
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn(key => {
if (key === 'GITHUB_TOKEN') return 'ghp_test_github_token_mock123456789012345678901234';
if (key === 'ANTHROPIC_API_KEY')
return 'sk-ant-test-anthropic-key12345678901234567890123456789';
return null;
})
}));
// Now require the module under test
const { execFileSync } = require('child_process');

View File

@@ -33,17 +33,6 @@ jest.mock('../../../src/utils/logger', () => ({
}));
// Mock secureCredentials before requiring modules that use it
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn(key => {
const mockCredentials = {
GITHUB_TOKEN: 'ghp_test_token_with_proper_prefix',
ANTHROPIC_API_KEY: 'test_anthropic_key',
GITHUB_WEBHOOK_SECRET: 'test_secret'
};
return mockCredentials[key] || null;
}),
has: jest.fn(() => true)
}));
const githubService =
require('../../../src/services/githubService').default ||

View File

@@ -36,17 +36,6 @@ jest.mock('../../../src/utils/logger', () => ({
}));
// Mock secureCredentials before requiring modules that use it
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn(key => {
const mockCredentials = {
GITHUB_TOKEN: 'ghp_test_token_with_proper_prefix',
ANTHROPIC_API_KEY: 'test_anthropic_key',
GITHUB_WEBHOOK_SECRET: 'test_secret'
};
return mockCredentials[key] || null;
}),
has: jest.fn(() => true)
}));
// Mock axios to avoid actual HTTP requests during tests
jest.mock('axios');