forked from claude-did-this/claude-hub
Compare commits
3 Commits
main
...
remove-n8n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871762add6 | ||
|
|
91b4cad93d | ||
|
|
e17313ac43 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
@@ -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';
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -12,9 +12,6 @@ jest.mock('../../../src/utils/logger', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../src/utils/secureCredentials', () => ({
|
||||
get: jest.fn()
|
||||
}));
|
||||
|
||||
const mockSecureCredentials = require('../../../src/utils/secureCredentials');
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,9 +11,6 @@ jest.mock('../../../src/utils/logger', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../src/utils/secureCredentials', () => ({
|
||||
get: jest.fn()
|
||||
}));
|
||||
|
||||
const mockSecureCredentials = require('../../../src/utils/secureCredentials');
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user