forked from claude-did-this/claude-hub
Compare commits
9 Commits
remove-n8n
...
feat/optim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7009a52b19 | ||
|
|
8fcff988ce | ||
|
|
50a667e205 | ||
|
|
65176a3b94 | ||
|
|
60732c1d72 | ||
|
|
971fe590f0 | ||
|
|
72037d47b2 | ||
|
|
d83836fc46 | ||
|
|
7ee3be8423 |
40
.github/workflows/docker-publish.yml
vendored
40
.github/workflows/docker-publish.yml
vendored
@@ -16,26 +16,16 @@ env:
|
||||
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'cheffromspace' }}
|
||||
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION || 'intelligenceassist' }}
|
||||
IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'claude-hub' }}
|
||||
# Runner configuration - set USE_SELF_HOSTED to 'false' to force GitHub-hosted runners
|
||||
USE_SELF_HOSTED: ${{ vars.USE_SELF_HOSTED || 'true' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# Use self-hosted runners by default, with ability to override via repository variable
|
||||
runs-on: ${{ vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || fromJSON('["self-hosted", "linux", "x64", "docker"]') }}
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Runner Information
|
||||
run: |
|
||||
echo "Running on: ${{ runner.name }}"
|
||||
echo "Runner OS: ${{ runner.os }}"
|
||||
echo "Runner labels: ${{ join(runner.labels, ', ') }}"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -110,9 +100,8 @@ jobs:
|
||||
|
||||
# Build claudecode separately
|
||||
build-claudecode:
|
||||
runs-on: ${{ vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || fromJSON('["self-hosted", "linux", "x64", "docker"]') }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -152,27 +141,4 @@ jobs:
|
||||
tags: ${{ steps.meta-claudecode.outputs.tags }}
|
||||
labels: ${{ steps.meta-claudecode.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Fallback job if self-hosted runners timeout
|
||||
build-fallback:
|
||||
needs: [build, build-claudecode]
|
||||
if: |
|
||||
always() &&
|
||||
(needs.build.result == 'failure' || needs.build-claudecode.result == 'failure') &&
|
||||
vars.USE_SELF_HOSTED != 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Trigger rebuild on GitHub-hosted runners
|
||||
run: |
|
||||
echo "Self-hosted runner build failed. To retry with GitHub-hosted runners:"
|
||||
echo "1. Set the repository variable USE_SELF_HOSTED to 'false'"
|
||||
echo "2. Re-run this workflow"
|
||||
echo ""
|
||||
echo "Or manually trigger a new workflow run with GitHub-hosted runners."
|
||||
exit 1
|
||||
cache-to: type=gha,mode=max
|
||||
13
Dockerfile
13
Dockerfile
@@ -88,25 +88,24 @@ RUN groupadd -g 999 docker 2>/dev/null || true \
|
||||
&& useradd -m -u 1001 -s /bin/bash claudeuser \
|
||||
&& usermod -aG docker claudeuser 2>/dev/null || true
|
||||
|
||||
# Create necessary directories and set permissions while still root
|
||||
# Create npm global directory for claudeuser and set permissions
|
||||
RUN mkdir -p /home/claudeuser/.npm-global \
|
||||
&& mkdir -p /home/claudeuser/.config/claude \
|
||||
&& chown -R claudeuser:claudeuser /home/claudeuser/.npm-global /home/claudeuser/.config
|
||||
&& chown -R claudeuser:claudeuser /home/claudeuser/.npm-global
|
||||
|
||||
# Configure npm to use the user directory for global packages
|
||||
USER claudeuser
|
||||
ENV NPM_CONFIG_PREFIX=/home/claudeuser/.npm-global
|
||||
ENV PATH=/home/claudeuser/.npm-global/bin:$PATH
|
||||
|
||||
# Switch to non-root user and install Claude Code
|
||||
USER claudeuser
|
||||
|
||||
# Install Claude Code (latest version) as non-root user
|
||||
# hadolint ignore=DL3016
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Switch back to root for remaining setup
|
||||
USER root
|
||||
|
||||
# Create claude config directory
|
||||
RUN mkdir -p /home/claudeuser/.config/claude
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy production dependencies from prod-deps stage
|
||||
|
||||
36
docker-compose.secrets.yml
Normal file
36
docker-compose.secrets.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
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,6 +9,10 @@ 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
|
||||
@@ -25,10 +29,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}
|
||||
# Secrets from environment variables
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
|
||||
# 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
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
|
||||
@@ -37,8 +41,16 @@ services:
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- claude-hub
|
||||
- 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
|
||||
|
||||
networks:
|
||||
claude-hub:
|
||||
driver: bridge
|
||||
n8n_default:
|
||||
external: true
|
||||
@@ -15,38 +15,14 @@ Our optimized Docker build pipeline includes:
|
||||
## Self-Hosted Runners
|
||||
|
||||
### Configuration
|
||||
- **Labels**: `self-hosted, linux, x64, docker`
|
||||
- **Usage**: All Docker builds use self-hosted runners by default for improved performance
|
||||
- **Local Cache**: Self-hosted runners maintain Docker layer cache between builds
|
||||
- **Fallback**: Configurable via `USE_SELF_HOSTED` repository variable
|
||||
- **Labels**: `self-hosted,Linux,X64,docker`
|
||||
- **Fallback**: Automatically falls back to GitHub-hosted runners if self-hosted are unavailable
|
||||
- **Strategy**: Uses self-hosted runners for main branch pushes, GitHub-hosted for PRs
|
||||
|
||||
### Runner Setup
|
||||
Self-hosted runners provide:
|
||||
- Persistent Docker layer cache
|
||||
- Faster builds (no image pull overhead)
|
||||
- Better network throughput for pushing images
|
||||
- Cost savings on GitHub Actions minutes
|
||||
|
||||
### Fallback Strategy
|
||||
The workflow implements a flexible fallback mechanism:
|
||||
|
||||
1. **Default behavior**: Uses self-hosted runners (`self-hosted, linux, x64, docker`)
|
||||
2. **Override option**: Set repository variable `USE_SELF_HOSTED=false` to force GitHub-hosted runners
|
||||
3. **Timeout protection**: 30-minute timeout prevents hanging on unavailable runners
|
||||
4. **Failure detection**: `build-fallback` job provides instructions if self-hosted runners fail
|
||||
|
||||
To manually switch to GitHub-hosted runners:
|
||||
```bash
|
||||
# Via GitHub UI: Settings → Secrets and variables → Actions → Variables
|
||||
# Add: USE_SELF_HOSTED = false
|
||||
|
||||
# Or via GitHub CLI:
|
||||
gh variable set USE_SELF_HOSTED --body "false"
|
||||
```
|
||||
|
||||
The runner selection logic:
|
||||
### Runner Selection Logic
|
||||
```yaml
|
||||
runs-on: ${{ fromJSON(format('["{0}"]', (vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || 'self-hosted, linux, x64, docker'))) }}
|
||||
# Main branch pushes → self-hosted runners (faster, local cache)
|
||||
# Pull requests → GitHub-hosted runners (save resources)
|
||||
```
|
||||
|
||||
## Multi-Stage Dockerfile
|
||||
|
||||
92
scripts/setup/setup-secure-credentials.sh
Executable file
92
scripts/setup/setup-secure-credentials.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/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,6 +10,7 @@ import {
|
||||
} from '../services/githubService';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { sanitizeBotMentions } from '../utils/sanitize';
|
||||
import secureCredentials from '../utils/secureCredentials';
|
||||
import type {
|
||||
WebhookHandler,
|
||||
WebhookRequest,
|
||||
@@ -67,9 +68,9 @@ function verifyWebhookSignature(req: WebhookRequest): boolean {
|
||||
'Verifying webhook signature'
|
||||
);
|
||||
|
||||
const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET;
|
||||
const webhookSecret = secureCredentials.get('GITHUB_WEBHOOK_SECRET');
|
||||
if (!webhookSecret) {
|
||||
logger.error('GITHUB_WEBHOOK_SECRET not found in environment');
|
||||
logger.error('GITHUB_WEBHOOK_SECRET not found in secure credentials');
|
||||
throw new Error('Webhook secret not configured');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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');
|
||||
|
||||
@@ -22,9 +23,11 @@ class DiscordProvider extends ChatbotProvider {
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
this.botToken = process.env.DISCORD_BOT_TOKEN;
|
||||
this.publicKey = process.env.DISCORD_PUBLIC_KEY;
|
||||
this.applicationId = process.env.DISCORD_APPLICATION_ID;
|
||||
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;
|
||||
|
||||
if (!this.botToken || !this.publicKey) {
|
||||
throw new Error('Discord bot token and public key are required');
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,
|
||||
@@ -51,7 +52,7 @@ export async function processCommand({
|
||||
'Processing command with Claude'
|
||||
);
|
||||
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
const githubToken = secureCredentials.get('GITHUB_TOKEN');
|
||||
|
||||
// In test mode, skip execution and return a mock response
|
||||
if (process.env['NODE_ENV'] === 'test' || !githubToken?.includes('ghp_')) {
|
||||
@@ -350,7 +351,7 @@ function createEnvironmentVars({
|
||||
OPERATION_TYPE: operationType,
|
||||
COMMAND: fullPrompt,
|
||||
GITHUB_TOKEN: githubToken,
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? ''
|
||||
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY') ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -507,7 +508,7 @@ function handleDockerExecutionError(
|
||||
// Sensitive values to redact
|
||||
const sensitiveValues = [
|
||||
context.githubToken,
|
||||
process.env.ANTHROPIC_API_KEY
|
||||
secureCredentials.get('ANTHROPIC_API_KEY')
|
||||
].filter(val => val && val.length > 0);
|
||||
|
||||
// Redact specific sensitive values first
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import secureCredentials from '../utils/secureCredentials';
|
||||
import type {
|
||||
CreateCommentRequest,
|
||||
CreateCommentResponse,
|
||||
@@ -22,7 +23,7 @@ let octokit: Octokit | null = null;
|
||||
|
||||
function getOctokit(): Octokit | null {
|
||||
if (!octokit) {
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
const githubToken = secureCredentials.get('GITHUB_TOKEN');
|
||||
if (githubToken?.includes('ghp_')) {
|
||||
octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
|
||||
135
src/utils/secureCredentials.ts
Normal file
135
src/utils/secureCredentials.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
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,6 +10,10 @@ 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,9 +4,28 @@ 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,6 +12,9 @@ jest.mock('../../../src/utils/logger', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../src/utils/secureCredentials', () => ({
|
||||
get: jest.fn()
|
||||
}));
|
||||
|
||||
const mockSecureCredentials = require('../../../src/utils/secureCredentials');
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ 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,6 +10,9 @@ jest.mock('../../../src/utils/logger', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../src/utils/secureCredentials', () => ({
|
||||
get: jest.fn().mockReturnValue('mock_value')
|
||||
}));
|
||||
|
||||
describe('Discord Payload Processing Tests', () => {
|
||||
let provider;
|
||||
|
||||
@@ -11,6 +11,9 @@ jest.mock('../../../src/utils/logger', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../src/utils/secureCredentials', () => ({
|
||||
get: jest.fn()
|
||||
}));
|
||||
|
||||
const mockSecureCredentials = require('../../../src/utils/secureCredentials');
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
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', () => ({
|
||||
@@ -41,6 +40,14 @@ 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,6 +33,17 @@ 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,6 +36,17 @@ 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