WHAT WAS DONE: - Created docs/tasks/arbiter-2x/ with README and implementation guide - Created docs/reference/gemini-consultations/ for AI partner records - Documented complete Arbiter 2.x architecture and implementation plan FILES ADDED: - docs/tasks/arbiter-2x/README.md (overview, phases, gotchas) - docs/tasks/arbiter-2x/IMPLEMENTATION-GUIDE.md (complete technical spec) - docs/reference/gemini-consultations/2026-03-31-arbiter-whitelist-architecture.md - docs/reference/gemini-consultations/2026-03-31-arbiter-implementation-details.md GEMINI CONSULTATIONS: Preserved complete Gemini AI architectural consultation from March 31, 2026. Includes: - Initial architecture consultation (unified app vs microservices) - Database schema design (PostgreSQL with indexes) - Minecraft account linking flow (Discord /link command) - Pterodactyl API integration (File Management API) - Complete code examples (Mojang validation, file write, cron sync) IMPLEMENTATION GUIDE INCLUDES: - 5-phase implementation plan with checklists - PostgreSQL schema with indexes for 500-user scale - Production-ready code snippets (pg pool, Mojang API, Panel API) - Critical gotchas (Content-Type, UUID dashes, HTTP 412) - Hourly cron reconciliation logic - Error handling and rate limiting strategies WHY: Task #90 is Tier 1 soft launch blocker. This documentation provides complete blueprint for implementing subscription-driven whitelist system. All architectural decisions validated by Gemini AI. NEXT STEPS: - Phase 1: PostgreSQL database setup - Phase 2: Core functions (Mojang, Panel API) - Phase 3: Discord /link command - Phase 4: Sync system (event-driven + cron) - Phase 5: Admin panel and testing Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
8.5 KiB
Gemini's Arbiter 2.x Implementation Guide
Complete Technical Specification
Date: 2026-03-31 Source: Gemini AI Architectural Consultation Status: READY TO IMPLEMENT
1. Pterodactyl File Management API
Writing whitelist.json
Endpoint: POST /api/client/servers/{server_identifier}/files/write?file=whitelist.json
Headers:
Authorization: Bearer <Client_API_Key>Accept: application/jsonContent-Type: text/plain← CRITICAL: Must be text/plain, not application/json!
Body: Raw stringified JSON array (not wrapped in JSON object)
const fileContent = JSON.stringify(whitelistArray, null, 2);
// Send as raw text body
Triggering Reload Command
Endpoint: POST /api/client/servers/{server_identifier}/command
Headers:
Authorization: Bearer <Client_API_Key>Content-Type: application/jsonAccept: application/json
Body:
{"command": "whitelist reload"}
Error Handling - CRITICAL!
HTTP 412 (Server Offline) is EXPECTED and SAFE:
- File write succeeds even if server offline
- Command fails with 412 = server will use new file on next boot
- DO NOT treat this as an error!
Rate Limiting:
- Do NOT use
Promise.all()for bulk syncs - Process servers sequentially or in small batches
- Prevents overwhelming Panel API
2. Auto-Discovery - Application API
Use Application API, not Client API for discovery!
Endpoint: GET /api/application/servers?include=allocations,node,nest
Headers: Authorization: Bearer <Application_API_Key>
Filtering Logic:
- Check
attributes.nest.attributes.name=== "Minecraft" - Check
attributes.node.attributes.namefor physical location (TX1/NC1) - Extract
identifier(8-char string for Client API calls)
Exclude servers:
- Filter out by nest type (FoundryVTT is different nest)
- OR check server name against EXCLUDED_SERVERS list
3. Whitelist JSON Format - EXACT SPECIFICATION
CRITICAL: Minecraft requires UUIDs WITH DASHES since 1.8+
Correct format:
[
{
"uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
"name": "Notch"
},
{
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Steve"
}
]
Why UUIDs matter:
- Username-only format forces server to query Mojang on boot
- Slows startup
- Can fail if Mojang API rate-limits server IP
- UUIDs are permanent, usernames can change
GOTCHA: Mojang API returns UUIDs WITHOUT dashes - must format before saving!
4. Database Schema Refinements
Add Indexes for 500-User Scale
CREATE INDEX idx_users_discord_id ON users(discord_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
New Table: Sync Tracking
CREATE TABLE server_sync_log (
server_identifier VARCHAR(50) PRIMARY KEY,
last_successful_sync TIMESTAMP,
last_error TEXT,
is_online BOOLEAN DEFAULT true
);
Purpose: Debug "Why isn't Steve whitelisted on Dallas node?"
5. Code Examples - Production Ready
A. PostgreSQL Connection (using pg pool)
// database.js
const { Pool } = require('pg');
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
max: 20, // Max connections for 500 user scale
idleTimeoutMillis: 30000
});
module.exports = {
query: (text, params) => pool.query(text, params),
};
B. Mojang API Validation + UUID Formatting
// mojang.js
function formatUUID(uuidStr) {
// Converts "069a79f444e94726a5befca90e38aaf5"
// to "069a79f4-44e9-4726-a5be-fca90e38aaf5"
return `${uuidStr.slice(0, 8)}-${uuidStr.slice(8, 12)}-${uuidStr.slice(12, 16)}-${uuidStr.slice(16, 20)}-${uuidStr.slice(20)}`;
}
async function validateMinecraftUser(username) {
try {
const res = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`);
// 204 or 404 = user not found
if (res.status === 204 || res.status === 404) return null;
if (!res.ok) throw new Error(`Mojang API error: ${res.status}`);
const data = await res.json();
return {
name: data.name, // Correct capitalization from Mojang
uuid: formatUUID(data.id) // ADD DASHES!
};
} catch (error) {
console.error("Failed to validate Minecraft user:", error);
return null;
}
}
C. Pterodactyl File Write
// panel.js
async function writeWhitelistFile(serverIdentifier, whitelistArray) {
const fileContent = JSON.stringify(whitelistArray, null, 2);
const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/files/write?file=whitelist.json`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`,
'Accept': 'application/json',
'Content-Type': 'text/plain' // MUST be text/plain!
},
body: fileContent
});
if (!res.ok) throw new Error(`Failed to write file: ${res.statusText}`);
return true;
}
D. Pterodactyl Reload Command (Safe Fail)
// panel.js (continued)
async function reloadWhitelistCommand(serverIdentifier) {
const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/command`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ command: "whitelist reload" })
});
// 412 = server offline, this is FINE!
if (res.status === 412) {
console.log(`[${serverIdentifier}] is offline. File saved for next boot.`);
return true;
}
if (!res.ok) throw new Error(`Command failed: ${res.statusText}`);
return true;
}
E. Hourly Cron Reconciliation
// sync.js
const cron = require('node-cron');
const db = require('./database');
const panel = require('./panel');
// Runs at minute 0 past every hour (1:00, 2:00, 3:00, etc.)
cron.schedule('0 * * * *', async () => {
console.log("Starting hourly whitelist reconciliation...");
// 1. Fetch Master Whitelist from Database
const { rows: players } = await db.query(
`SELECT minecraft_username as name, minecraft_uuid as uuid
FROM users
JOIN subscriptions ON users.discord_id = subscriptions.discord_id
WHERE subscriptions.status = 'active'`
);
// 2. Fetch Active Servers via Auto-Discovery
const servers = await panel.getServers();
// 3. Sync Sequentially (prevents rate limiting)
for (const server of servers) {
try {
await panel.writeWhitelistFile(server.identifier, players);
await panel.reloadWhitelistCommand(server.identifier);
// Log successful sync
await db.query(
"UPDATE server_sync_log SET last_successful_sync = NOW() WHERE server_identifier = $1",
[server.identifier]
);
} catch (err) {
console.error(`Sync failed for ${server.identifier}:`, err);
// Log failed sync
await db.query(
"UPDATE server_sync_log SET last_error = $1 WHERE server_identifier = $2",
[err.message, server.identifier]
);
}
}
console.log("Hourly reconciliation complete.");
});
Key Gotchas & Warnings
- Content-Type MUST be text/plain for file write endpoint (not application/json)
- UUIDs MUST include dashes in whitelist.json (Mojang returns without dashes)
- HTTP 412 is not an error - server offline, file saved for next boot
- Sequential processing prevents rate limiting (don't use Promise.all)
- Application API for discovery, Client API for file operations
- Mojang API can rate-limit - validate once during /link, store permanently
Implementation Order
Phase 1: Database Migration
- Set up PostgreSQL
- Create tables with indexes
- Migrate existing Arbiter data
Phase 2: Core Functions
- Database connection pool
- Mojang validation
- Pterodactyl file write/command functions
Phase 3: Discord Integration
/linkslash command- Auto-DM new subscribers
- Role assignment webhook handler
Phase 4: Sync System
- Event-driven sync (on /link, on subscribe)
- Hourly cron reconciliation
- Sync logging for debugging
Phase 5: Admin Panel
- View sync status
- Manual sync trigger
- View linked accounts
Fire + Frost + Foundation = Where Love Builds Legacy 💙🔥❄️
Implementation Status: READY TO BUILD Next Step: Create Task #90 - Arbiter 2.x Implementation