Files
Claude (The Golden Chronicler #50) 04e9b407d5 feat: Migrate Arbiter and Modpack Version Checker to monorepo
WHAT WAS DONE:
- Migrated Arbiter (discord-oauth-arbiter) code to services/arbiter/
- Migrated Modpack Version Checker code to services/modpack-version-checker/
- Created .env.example for Arbiter with all required environment variables
- Moved systemd service file to services/arbiter/deploy/
- Organized directory structure per Gemini monorepo recommendations

WHY:
- Consolidate all service code in one repository
- Prepare for Gemini code review (Panel v1.12 compatibility check)
- Enable service-prefixed Git tagging (arbiter-v2.1.0, modpack-v1.0.0)
- Support npm workspaces for shared dependencies

SERVICES MIGRATED:
1. Arbiter (Discord OAuth bot) - Originally written by Gemini + Claude
   - Full source code from ops-manual docs/implementation/
   - Created comprehensive .env.example
   - Ready for Panel v1.12 compatibility verification

2. Modpack Version Checker (Python CLI tool)
   - Full source code from ops-manual docs/tasks/
   - Written for Panel v1.11, needs Gemini review for v1.12
   - Never had code review before

STILL TODO:
- Whitelist Manager - Pull from Billing VPS (38.68.14.188)
  - Currently deployed and running
  - Needs Panel v1.12 API compatibility fix (Task #86)
  - Requires SSH access to pull code

NEXT STEPS:
- Gemini code review for Panel v1.12 API compatibility
- Create package.json for each service
- Test npm workspaces integration
- Deploy after verification

FILES:
- services/arbiter/ (25 new files, full application)
- services/modpack-version-checker/ (21 new files, full application)

Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
2026-03-31 21:52:42 +00:00

12 KiB

Firefrost Arbiter

Discord Role Management and Subscription OAuth Gateway

A centralized Node.js/Express service for managing Discord community roles, authenticating users via OAuth2, and processing subscription webhooks from Paymenter billing platform.


🎯 Purpose

Firefrost Arbiter automates the entire subscription-to-Discord-role workflow:

  1. User subscribes via Paymenter (billing system)
  2. Webhook fires to Arbiter
  3. Email sent with secure 24-hour linking URL
  4. User clicks link → Discord OAuth → Role assigned automatically
  5. Ghost CMS updated with Discord ID for future reference

Admin Interface allows Trinity members to manually assign/remove roles, search subscribers, and view audit logs.


🏗️ Architecture Overview

Components

  • Webhook Gateway: Receives subscription events from Paymenter, generates secure single-use tokens, dispatches notification emails via SMTP
  • OAuth2 Linking: Authenticates users via Discord, updates Ghost CMS member metadata, automatically assigns Discord server roles based on subscription tiers
  • Admin Dashboard: Protected by Discord OAuth (restricted to specific User IDs), allows staff to manually assign roles, view audit logs, and search CMS records
  • State Management: Utilizes local SQLite databases (linking.db and sessions.db) for lightweight, persistent data storage

Tech Stack

  • Runtime: Node.js 18.x+
  • Framework: Express 4.x
  • Database: SQLite (better-sqlite3)
  • Discord: discord.js 14.x
  • CMS: Ghost 5.x (Admin API)
  • Email: Nodemailer (SMTP)
  • Session: express-session + connect-sqlite3
  • Security: express-rate-limit, Zod validation, HMAC webhook verification

📋 Prerequisites

Before installation, ensure you have:

  • Node.js 18.x or higher installed
  • A Discord Application with Bot User created (Discord Developer Portal)
    • Server Members Intent enabled
    • Bot invited to your Discord server with "Manage Roles" permission
    • Bot role positioned ABOVE all subscription tier roles in role hierarchy
  • Ghost CMS 5.x with Admin API access
    • Custom field discord_id created in Ghost Admin
    • Integration created for Admin API key
  • SMTP Server for outgoing mail (Mailcow, Gmail, SendGrid, etc.)
  • Nginx for reverse proxy and SSL termination
  • SSL Certificate (Let's Encrypt recommended)

🚀 Installation

1. Clone and Install Dependencies

cd /home/architect
git clone <repository-url> arbiter
cd arbiter
npm install

2. Configure Environment Variables

cp .env.example .env
nano .env

Fill in all required values:

  • Discord credentials (bot token, client ID/secret, guild ID)
  • Ghost CMS URL and Admin API key
  • SMTP server details
  • Admin Discord IDs (comma-separated, no spaces)
  • Generate SESSION_SECRET: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  • Webhook secret from Paymenter

3. Configure Discord Role Mapping

Edit config/roles.json with your Discord role IDs:

nano config/roles.json

Get role IDs: Right-click role in Discord → Copy ID

4. Set Up Nginx Reverse Proxy

sudo cp nginx.conf /etc/nginx/sites-available/arbiter
sudo ln -s /etc/nginx/sites-available/arbiter /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

5. Set Up Systemd Service

sudo cp arbiter.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable arbiter
sudo systemctl start arbiter

6. Verify Installation

Check service status:

sudo systemctl status arbiter

Check logs:

sudo journalctl -u arbiter -f

Visit health check:

curl https://discord-bot.firefrostgaming.com/health

Should return:

{
  "uptime": 123.456,
  "discord": "ok",
  "database": "ok",
  "timestamp": "2026-03-30T15:00:00.000Z"
}

7. Set Up Automated Backups

chmod +x backup.sh
crontab -e

Add this line (runs daily at 4:00 AM):

0 4 * * * /home/architect/arbiter/backup.sh >> /home/architect/backups/arbiter/cron_error.log 2>&1

Create backup directory:

mkdir -p /home/architect/backups/arbiter
chmod 700 /home/architect/backups/arbiter

🔧 Configuration

Discord Developer Portal Setup

  1. Go to Discord Developer Portal
  2. Create New Application (or select existing)
  3. Bot Tab:
    • Generate bot token (save to .env as DISCORD_BOT_TOKEN)
    • Enable "Server Members Intent"
  4. OAuth2 → General:
    • Add Redirect URIs:
      • http://localhost:3500/auth/callback (testing)
      • https://discord-bot.firefrostgaming.com/auth/callback (production)
      • https://discord-bot.firefrostgaming.com/admin/callback (admin login)
  5. OAuth2 → URL Generator:
    • Scopes: bot
    • Permissions: Manage Roles
    • Copy generated URL and invite bot to server

CRITICAL: In Discord Server Settings → Roles, drag the bot's role ABOVE all subscription tier roles!

Ghost CMS Setup

  1. Create Custom Field:

    • Navigate to: Settings → Membership → Custom Fields
    • Add field: discord_id (type: Text)
  2. Generate Admin API Key:

    • Navigate to: Settings → Integrations → Add Custom Integration
    • Name: "Firefrost Arbiter"
    • Copy Admin API Key (format: key_id:secret)
    • Save to .env as CMS_ADMIN_KEY

Paymenter Webhook Configuration

  1. In Paymenter admin panel, navigate to Webhooks
  2. Add new webhook:
    • URL: https://discord-bot.firefrostgaming.com/webhook/billing
    • Secret: (generate secure random string, save to .env as WEBHOOK_SECRET)
    • Events: subscription.created, subscription.upgraded, subscription.downgraded, subscription.cancelled

📖 Usage

For Subscribers (Automated Flow)

  1. User subscribes via Paymenter
  2. User receives email with secure linking URL
  3. User clicks link → redirected to Discord OAuth
  4. User authorizes → role automatically assigned
  5. User sees new channels in Discord immediately

For Admins (Manual Assignment)

  1. Visit https://discord-bot.firefrostgaming.com/admin/login
  2. Authenticate via Discord
  3. Search user by email (from Ghost CMS)
  4. Assign role or remove all roles
  5. Provide reason (logged to audit trail)
  6. View audit log of all manual actions

🧪 Testing

Local Testing Setup

  1. Set APP_URL=http://localhost:3500 in .env
  2. Add http://localhost:3500/auth/callback to Discord redirect URIs
  3. Run in development mode:
npm run dev

Test Webhook Reception

curl -X POST http://localhost:3500/webhook/billing \
  -H "Content-Type: application/json" \
  -H "x-signature: test_signature" \
  -d '{
    "event": "subscription.created",
    "customer_email": "test@example.com",
    "customer_name": "Test User",
    "tier": "awakened",
    "subscription_id": "test_sub_123"
  }'

Test OAuth Flow

  1. Trigger webhook (above) to generate token
  2. Check database: sqlite3 linking.db "SELECT * FROM link_tokens;"
  3. Check email sent (Mailcow logs)
  4. Click link in email
  5. Complete Discord OAuth
  6. Verify role assigned in Discord
  7. Verify Ghost CMS updated: Ghost Admin → Members → search email

Test Admin Panel

  1. Visit /admin/login
  2. Authenticate with Discord
  3. Search for test user by email
  4. Assign/remove role
  5. Check audit log displays action

📁 Project Structure

arbiter/
├── src/
│   ├── routes/
│   │   ├── webhook.js       # Paymenter webhook handler
│   │   ├── oauth.js          # User Discord linking flow
│   │   ├── admin.js          # Admin panel routes
│   │   └── adminAuth.js      # Admin OAuth login
│   ├── middleware/
│   │   ├── auth.js           # Admin access control
│   │   ├── verifyWebhook.js  # HMAC signature verification
│   │   └── validateWebhook.js # Zod schema validation
│   ├── utils/
│   │   └── templates.js      # HTML success/error pages
│   ├── views/
│   │   └── admin.html        # Admin panel UI
│   ├── database.js           # SQLite initialization
│   ├── email.js              # Nodemailer SMTP
│   ├── discordService.js     # Bot client + role management
│   ├── cmsService.js         # Ghost CMS integration
│   └── index.js              # Main application entry
├── config/
│   └── roles.json            # Tier → Discord Role ID mapping
├── .env                      # Environment variables (not in git)
├── .env.example              # Template for .env
├── package.json              # Dependencies
├── backup.sh                 # Automated backup script
├── arbiter.service           # Systemd service file
└── nginx.conf                # Nginx reverse proxy config

🔐 Security

  • Webhook Verification: HMAC SHA256 signature validation
  • Input Validation: Zod schemas for all webhook payloads
  • Rate Limiting: 100 requests per 15 minutes per IP
  • Session Security: httpOnly, SameSite cookies
  • Admin Access Control: Discord ID whitelist via environment variable
  • HTTPS Enforcement: Nginx SSL termination with HSTS headers
  • Secure Tokens: 32-byte cryptographically random tokens (64 hex chars)
  • Token Expiration: 24-hour automatic expiry
  • Database Permissions: chmod 700 on backup directory

🛠️ Maintenance

View Logs

# Application logs
sudo journalctl -u arbiter -f

# Nginx access logs
sudo tail -f /var/log/nginx/arbiter-access.log

# Nginx error logs
sudo tail -f /var/log/nginx/arbiter-error.log

# Backup logs
tail -f /home/architect/backups/arbiter/backup_log.txt

Restart Service

sudo systemctl restart arbiter

Update Application

cd /home/architect/arbiter
git pull
npm install
sudo systemctl restart arbiter

Database Maintenance

# View link tokens
sqlite3 linking.db "SELECT * FROM link_tokens WHERE used = 0;"

# View audit logs
sqlite3 linking.db "SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 10;"

# Manual cleanup of expired tokens
sqlite3 linking.db "DELETE FROM link_tokens WHERE created_at < datetime('now', '-1 day');"

🚨 Troubleshooting

See TROUBLESHOOTING.md for detailed solutions to common issues.

Quick reference:

  • Invalid redirect URI → Check Discord Developer Portal OAuth settings
  • Bot missing permissions → Check role hierarchy in Discord
  • Session not persisting → Check trust proxy setting in code
  • Ghost API 401 → Verify Admin API key format
  • Database locked → Increase timeout in database.js
  • Email not sending → Check SMTP credentials and port 587 firewall rule

📦 Backup & Restore

Backup Procedure

Automated daily at 4:00 AM via cron. Manual backup:

./backup.sh

Backs up:

  • linking.db (tokens and audit logs)
  • sessions.db (admin sessions)
  • .env (configuration with secrets)
  • config/roles.json (tier mappings)

Retention: 7 days

Restore Procedure

# Stop service
sudo systemctl stop arbiter

# Move corrupted database
mv linking.db linking.db.corrupt

# Restore from backup
cp /home/architect/backups/arbiter/linking_YYYYMMDD_HHMMSS.db linking.db

# Start service
sudo systemctl start arbiter

Verify restored backup:

sqlite3 /home/architect/backups/arbiter/linking_20260330_040000.db "SELECT count(*) FROM link_tokens;"

📚 Documentation


🤝 Contributing

This is a private system for Firefrost Gaming. For internal team members:

  1. Create feature branch
  2. Test locally
  3. Commit with detailed messages
  4. Deploy to staging first
  5. Monitor logs before production rollout

📝 License

Private - Firefrost Gaming Internal Use Only


👥 Team

Built by:

  • Michael "Frostystyle" Krause (The Wizard) - Technical Lead
  • Claude (Chronicler #49) - Implementation Partner
  • Gemini AI - Architecture Consultant

For: Firefrost Gaming Community

Date: March 30, 2026


🔥❄️ Fire + Frost + Foundation = Where Love Builds Legacy 💙