Security & Contributing Guide

Last Updated: 2026-05-26 • For Security Engineers & Contributors

Overview

This guide covers the authentication architecture, security best practices, rate limiting implementation, and contribution workflow for QuranBot.

  • Role-based permission system
  • Rate limiting & cooldown implementation
  • Security practices & token handling
  • Contribution workflow & PR guidelines

Authentication Architecture

QuranBot implements a multi-layered authentication system via src/auth/auth-manager.js that handles permissions at three levels: Discord native permissions, role-based keywords, and special developer users.

Authentication Layers

Layer Implementation Use Case
hasAdminPermission() Checks PermissionsBitField.Flags.Administrator Native Discord admin permission (bypasses all checks)
hasAdminRole() Keyword match against permissions_config.admin_roles Server-specific admin roles (e.g., "Quran Admin", "Islamic Admin")
isSpecialUser() Checks global.SPE_USER_IDS from env Bot developers with global access across all servers

Control Mode System

The bot supports two control modes per guild, stored in guildState.controlMode:

Mode: 'admins' (Default)
  1. Only users with admin permission/role or special user ID can interact with control panel
  2. Public features (prayer times, azkar, guide) remain accessible to everyone
  3. Commands like /إعداد, /دخول, /خروج require admin
Mode: 'everyone'
  1. Any member can use navigation buttons (prev/next surah, reciter selection)
  2. Admin-only actions (join/leave voice, mode toggle) still require authorization
  3. Cooldowns apply to prevent abuse (90s for public actions in this mode)
// src/auth/auth-manager.js - Core authorization logic
function isAuthorized(interaction, guildState, interactionType) {
    const member = interaction.member;
    const userId = interaction.user?.id;
    
    // Support/help always open
    if (interactionType === 'support' || interactionType === 'open_complaint_modal') {
        return true;
    }
    
    // Admin/special users bypass everything
    if (hasAdminPermission(member) || hasAdminRole(member) || isSpecialUser(userId)) {
        return true;
    }
    
    // Control mode check
    if (!guildState?.controlMode || guildState.controlMode === 'admins') {
        return false;
    }
    
    // Everyone mode - check whitelist
    return allowed_everyone_actions.includes(interactionType);
}

Permission Flow Diagram

Every interaction passes through this authorization pipeline in src/interactions/interactionProcessor.js:

Interaction Processing Pipeline:
  1. Global Cooldown Check - checkGlobalCooldown() blocks spam at user level
  2. Public Feature Detection - isPublicFeature() allows azkar/prayer buttons without auth
  3. Voice State Validation - checkVoiceState() ensures bot is in voice for playback actions
  4. Authorization Check - checkAuthorization() applies permission logic
  5. Voice-Specific Cooldown - checkVoiceCooldown() for join/leave actions
  6. Handler Execution - Route to button/menu/command handler

Exempt Actions (No Auth Required)

These interaction types bypass authorization checks entirely:

  • submit_complaint, open_complaint_modal - Support flows
  • play_azkar_* - Azkar audio playback buttons
  • prayer_times, select_country_prayer, select_city_prayer - Prayer time selection
  • more_features, back_to_main - Navigation helpers
  • toggle_control_mode - Mode switching (handled separately)

Cooldown System Deep Dive

QuranBot implements a dual-layer cooldown system: per-command cooldowns and global rate limiting.

Command Cooldowns (src/state/commandCooldown.js)

Command Duration Scope Key Mapping
/تحكم 25s Per user control
/دخول 15s Per user join
/خروج 15s Per user leave
/إعداد 60s Per server setup
/مواقيت_الصلاة 50s Per user prayerTimes
prayer_times button 15s Per user prayerTimesButton

Implementation Pattern

// In command handler (e.g., src/commands/join.js)
const cdResult = coreLoader.checkCooldown(
    interaction.user.id,
    interaction.guildId,
    'join'
);

if (!cdResult.allowed) {
    await safeError(
        interaction,
        coreLoader.getCooldownResponse(cdResult.remaining, cdResult.type)
    );
    return;
}

// ... execute command logic ...

// Set cooldown AFTER successful execution
coreLoader.setCooldown(interaction.user.id, interaction.guildId, 'join');

Memory Management

Cooldown data is stored in Map objects with automatic cleanup:

  • userCD Map: Keys are "userId:commandKey"
  • serverCD Map: Keys are "guildId:commandKey"
  • Cleanup interval runs every 60s to remove expired entries

Rate Limiting Implementation

Beyond per-command cooldowns, QuranBot implements global rate limiting to prevent abuse across all interactions.

Global Rate Limit (src/state/cooldown.js)

// Default: 100 actions per 60 seconds per user+guild combination
function checkRateLimit(userId, guildId, maxActions = 100, windowMs = 60000) {
    const now = Date.now();
    const key = `${userId}:${guildId}`;
    
    if (!rateLimitTracker.has(key)) {
        rateLimitTracker.set(key, { count: 1, windowStart: now });
        return { valid: true, count: 1, limit: maxActions };
    }
    
    const tracker = rateLimitTracker.get(key);
    
    // Reset window if expired
    if (now - tracker.windowStart > windowMs) {
        tracker.count = 1;
        tracker.windowStart = now;
        return { valid: true, count: 1, limit: maxActions };
    }
    
    tracker.count++;
    
    if (tracker.count > maxActions) {
        return {
            valid: false,
            count: tracker.count,
            limit: maxActions,
            message: `تم تجاوز حد الاستخدام المسموح. يرجى الانتظار ${Math.ceil((windowMs - (now - tracker.windowStart)) / 1000)} ثانية`,
        };
    }
    
    return { valid: true, count: tracker.count, limit: maxActions };
}

Interaction Deduplication

To prevent duplicate processing of rapid clicks, src/interactions/interactionHandler.js implements a 500ms deduplication window:

// Generate unique interaction ID
const interactionId = `${interaction.guildId}-${interaction.user.id}-${interaction.id}`;

// Check if this exact interaction was processed recently
if (global.interactionRateLimits && global.interactionRateLimits.has(interactionId)) {
    const lastTime = global.interactionRateLimits.get(interactionId);
    const now = Date.now();
    
    if (now - lastTime < 500) { // 500ms minimum interval
        coreLoader.logger.debug(`Ignored Fast Duplicate Interaction ${interactionId}`);
        return;
    }
}

// Record this interaction
global.interactionRateLimits.set(interactionId, Date.now());
Security Note: Rate Limit Storage

Rate limit data is stored in-memory only and resets on bot restart. This is intentional: persistent rate limiting across restarts could unfairly penalize users after maintenance. For production deployments requiring persistent rate limiting, consider integrating with Redis using the @database/redis module.

Security Best Practices

Environment Security

  • Never commit secrets - Use .env.example files as templates only
  • Separate environments - development.env and production.env prevent accidental use of production tokens in dev
  • TOPGG_TOKEN stripping - envSwitcher.js automatically removes this token in development mode
  • Rotate tokens regularly - Especially after team member changes or suspected compromise

Input Validation Patterns

All user inputs that affect bot behavior should be validated before use:

// Example: Validating surah selection in menu handler
const selectedSurahNum = parseInt(interaction.values[0]);

// Validate range
if (selectedSurahNum < 1 || selectedSurahNum > global.surahNames.length) {
    await safeError(interaction, 'Invalid surah selection');
    return;
}

// Validate availability for current reciter
if (!isSurahAvailable(guildState, surahIndex)) {
    await safeError(interaction, 'This surah is not available for the current reciter');
    return;
}

SQL/NoSQL Injection Prevention

Since QuranBot uses Firebase Realtime Database (NoSQL), injection risks are lower, but still follow these practices:

  • Use deepCloneForFirebase() to sanitize objects before storage
  • Never use user input directly in database paths - always validate and sanitize
  • Use Firebase security rules (firebase.rules.json) to deny public access

Discord API Security

  • Use ephemeral replies for sensitive information (flags: 64)
  • Validate interaction ownership - Ensure buttons/menus are used by the original requester when appropriate
  • Handle interaction expiration - Catch 10062 Unknown interaction errors gracefully
  • Limit embed content - Sanitize user-generated content before including in embeds

Token & Secret Handling

Firebase Private Key Sanitization

The getSanitizedPrivateKey() function in src/database/firebase/client.js handles common formatting issues:

function getSanitizedPrivateKey() {
    const rawKey = process.env.FIREBASE_ADMIN_PRIVATE_KEY;
    if (!rawKey) return '';
    
    let key = rawKey;
    
    // Fix escaped newlines from dotenv parsing
    key = key.replace(/\\n/g, '\n');
    key = key.replace(/\\r\\n/g, '\n');
    
    // Remove surrounding quotes if present
    if (key.startsWith('"') && key.endsWith('"')) {
        key = key.slice(1, -1);
    }
    
    // Ensure trailing newline for PEM format
    if (key && !key.endsWith('\n')) {
        key += '\n';
    }
    
    return key;
}

Token Loading Flow

  1. .env sets NODE_ENV (development/production)
  2. envSwitcher.js loads the corresponding *.env file
  3. In development: TOPGG_TOKEN is stripped from process.env
  4. Bot modules access tokens via process.env or global.token
Production Deployment Tip

For cloud deployments (Heroku, Railway, etc.), set environment variables via the platform's dashboard or CLI, not via .env files. Never commit .env files to version control.

Input Validation Patterns

Common Validation Utilities

Utility Location Use Case
validateReciterData() src/data/data-loader-validator.js Ensures reciter has required rewaya_id and server
validateRadioData() src/data/data-loader-validator.js Checks radio entry has non-empty url
validateSurahId() src/data/data-loader-validator.js Validates surah ID is between 1-114
clean_Dhikr() src/helpers/azkar.js Removes annotation chars from dhikr text before display

Sanitization for Display

When displaying user-generated or external content, sanitize to prevent XSS or layout issues:

// Example: Truncating text for Discord embed fields (max 1024 chars)
function truncateText(text, maxLength) {
    if (!text) return '';
    const str = String(text);
    return str.length > maxLength ? str.substring(0, maxLength) : str;
}

// Usage in UI components
const safeLabel = truncateText(reciter.name, 100);

URL Validation for Audio Streams

Before playing audio, validate stream URLs to prevent playback errors:

// src/audio/resource.js - validateStreamUrl function
async function validateStreamUrl(url) {
    if (!url?.startsWith('http')) return { valid: false, reason: 'Invalid URL' };
    
    try {
        const client = require('@startup/botSetup').client;
        if (!client.lavalink) return { valid: false, reason: 'Lavalink unavailable' };
        
        const bestNode = getBestNode(client.lavalink);
        const nodes = bestNode ? [bestNode] : client.lavalink.nodeManager.leastUsedNodes('players').filter((n) => n.connected);
        if (!nodes.length) return { valid: false, reason: 'No connected nodes' };
        
        const result = await nodes[0].search({ query: url, source: 'http' }, client.user);
        return { valid: result.tracks && result.tracks.length > 0, reason: 'OK' };
    } catch (error) {
        return { valid: false, reason: error.message };
    }
}

Contributing Guidelines

Before You Start

  • Check existing issues - Search GitHub for similar feature requests or bug reports
  • Discuss major changes - Open an issue or join Discord to discuss architecture changes before coding
  • Test locally - Ensure your changes work in a test server before submitting

Branch Naming Convention

Branch Type Format Example
Feature feature/descriptive-name feature/add-surah-favorites
Bug Fix fix/issue-description fix/voice-disconnect-recovery
Documentation docs/section-updated docs/add-security-guide
Refactor refactor/module-name refactor/auth-manager-cleanup

Commit Message Format

# Format: type(scope): brief description
# Types: feat, fix, docs, style, refactor, test, chore

# Good examples:
feat(audio): add fallback reciter selection on stream failure
fix(auth): handle expired interactions in permission checks
docs(security): add token handling best practices guide
refactor(state): simplify guild state initialization logic

# Avoid:
"fixed stuff"
"update code"
"wip"

Pull Request Workflow

1

Fork & Clone

Fork the repository and clone your fork locally. Add upstream remote for syncing.

2

Create Feature Branch

Use the branch naming convention. Keep branches focused on a single change.

3

Implement & Test

Write code following existing patterns. Test in a real Discord server. Run pnpm run dev for hot reload.

4

Run Linting

Ensure code passes style checks: pnpm run lint (if configured) or follow prettier.config.js.

5

Submit PR

Open PR with clear title, description of changes, and any relevant issue links. Request review from maintainers.

6

Address Feedback

Respond to review comments promptly. Push additional commits to the same branch; they'll auto-append to the PR.

Code Style & Standards

JavaScript Conventions

  • Indentation: 4 spaces (configured in prettier.config.js)
  • Quotes: Single quotes for strings, template literals for interpolation
  • Semicolons: Always use semicolons
  • Trailing commas: Use in multiline objects/arrays
  • Arrow functions: Use for callbacks and short functions; named functions for complex logic

Module Organization

// 1. External dependencies first
const { EmbedBuilder } = require('discord.js');

// 2. Internal modules with path aliases
require('pathlra-aliaser')();
const logger = require('@logging/logger');
const { safeReply } = require('@interactions/flow/responder');

// 3. Local relative imports last
const { createControlEmbed } = require('../ui/embeds');

// 4. Module exports at bottom
module.exports = { execute };

Naming Conventions

Element Convention Example
Variables/Functions camelCase createSurahResource, guildState
Constants UPPER_SNAKE_CASE COMMAND_COOLDOWNS, MAX_RETRY_ATTEMPTS
Classes PascalCase PersistentStateManager, VoiceLogger
Files kebab-case.js guild-state-store.js, commandCooldown.js
Commands Arabic names for user-facing, English keys internally /تحكم'control'

Testing Requirements

Manual Testing Checklist

Before submitting a PR, verify these scenarios in a test Discord server:

  • ☐ Command works with valid inputs
  • ☐ Command handles invalid inputs gracefully (error message, no crash)
  • ☐ Permissions are enforced correctly (admin vs. everyone mode)
  • ☐ Cooldowns trigger appropriately
  • ☐ Voice operations work (join, play, pause, leave)
  • ☐ State persists across bot restarts (if applicable)
  • ☐ No memory leaks after repeated use (monitor with /سرعة)

Logging for Debugging

Use the centralized logger instead of console.log for consistent output:

const logger = require('@logging/logger');

// Log levels: debug, info, warn, error, fatal
logger.info(`User ${userId} triggered command ${commandName}`);
logger.warn(`Fallback reciter used for guild ${guildId}`);
logger.error('Failed to fetch surah', error, { surahIndex, reciter });

// Voice-specific logging
const voiceLogger = require('@logging/voiceLogger');
voiceLogger.connection(guildId, 'Voice connection established', { channelId });

Using the /سرعة Diagnostic Command

This command provides real-time metrics for debugging:

  • Bot latency, WebSocket ping, Discord API response time
  • Uptime, server count, total users
  • Voice connection count, CPU usage
  • Lavalink node status (ping, players, uptime)
  • Commands used, azkar sent (from Firebase stats)

Debugging Techniques

Enabling Debug Logging

Set LOG_LEVEL=debug in your environment to see detailed logs:

# In .env or command line
LOG_LEVEL=debug NODE_ENV=development pnpm run dev
# Logs will show:
# - Interaction routing decisions
# - Voice connection state changes
# - Cache hits/misses
# - Database read/write operations

Voice-Specific Debugging

Use voiceLogger for granular audio debugging:

const voiceLogger = require('@logging/voiceLogger');

// Log connection events
voiceLogger.connection(guildId, 'Initializing Lavalink player', {
    channelId: targetChannel.id,
    guildName: targetChannel.guild?.name,
});

// Log playback events
voiceLogger.player(guildId, 'Track started', { title: track.info?.title });

// Log resource creation
voiceLogger.resource(guildId, 'Creating surah resource', {
    surahIndex,
    reciter: state.currentReciter,
});

Common Debugging Scenarios

Bot not responding to commands

Check:

  • Bot has Application (bot) scope with applications.commands in OAuth URL
  • Commands are registered (logger.info should show count)
  • Bot has Message Content intent enabled in Discord Developer Portal
  • No interaction cooldown or rate limit blocking the request
Voice connection fails with "No compatible encryption modes"

This indicates a Lavalink version mismatch. Verify:

  • Lavalink server is v4.x (required for Discord's current encryption)
  • secure setting matches your deployment (false for HTTP, true for HTTPS)
  • Bot and Lavalink are on compatible Node.js versions (v22 recommended)

Use /سرعة to check node status and error messages.

State not persisting after restart

Verify:

  • Firebase credentials are correct in production.env
  • global.saveRuntimeStates is called after state changes
  • Redis is available (if used) for hot caching
  • Check storage/logs/ for Firebase connection errors

Test by making a state change, restarting the bot, and verifying the state is restored.

Memory Profiling

Use Node.js built-in tools to identify memory leaks:

# Start bot with heap snapshot capability
node --heap-prof src/bot/core.js

# Or use Chrome DevTools
node --inspect src/bot/core.js

# Then open chrome://inspect in Chrome

# Monitor memory usage via /سرعة command
# Alert if heapUsed exceeds 1.5GB (triggers aggressive GC)
Debugging in Production

Never enable LOG_LEVEL=debug in production environments. Use structured logging with appropriate levels (info, warn, error) and rely on the /سرعة command for operational monitoring.