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>
330 lines
8.5 KiB
Markdown
330 lines
8.5 KiB
Markdown
# 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/json`
|
|
- `Content-Type: text/plain` ← **CRITICAL: Must be text/plain, not application/json!**
|
|
|
|
**Body:** Raw stringified JSON array (not wrapped in JSON object)
|
|
|
|
```javascript
|
|
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/json`
|
|
- `Accept: application/json`
|
|
|
|
**Body:**
|
|
```json
|
|
{"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:**
|
|
1. Check `attributes.nest.attributes.name` === "Minecraft"
|
|
2. Check `attributes.node.attributes.name` for physical location (TX1/NC1)
|
|
3. 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:**
|
|
```json
|
|
[
|
|
{
|
|
"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
|
|
|
|
```sql
|
|
CREATE INDEX idx_users_discord_id ON users(discord_id);
|
|
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
|
|
```
|
|
|
|
### New Table: Sync Tracking
|
|
|
|
```sql
|
|
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)
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
1. **Content-Type MUST be text/plain** for file write endpoint (not application/json)
|
|
2. **UUIDs MUST include dashes** in whitelist.json (Mojang returns without dashes)
|
|
3. **HTTP 412 is not an error** - server offline, file saved for next boot
|
|
4. **Sequential processing** prevents rate limiting (don't use Promise.all)
|
|
5. **Application API for discovery**, Client API for file operations
|
|
6. **Mojang API can rate-limit** - validate once during /link, store permanently
|
|
|
|
---
|
|
|
|
## Implementation Order
|
|
|
|
**Phase 1: Database Migration**
|
|
1. Set up PostgreSQL
|
|
2. Create tables with indexes
|
|
3. Migrate existing Arbiter data
|
|
|
|
**Phase 2: Core Functions**
|
|
1. Database connection pool
|
|
2. Mojang validation
|
|
3. Pterodactyl file write/command functions
|
|
|
|
**Phase 3: Discord Integration**
|
|
1. `/link` slash command
|
|
2. Auto-DM new subscribers
|
|
3. Role assignment webhook handler
|
|
|
|
**Phase 4: Sync System**
|
|
1. Event-driven sync (on /link, on subscribe)
|
|
2. Hourly cron reconciliation
|
|
3. Sync logging for debugging
|
|
|
|
**Phase 5: Admin Panel**
|
|
1. View sync status
|
|
2. Manual sync trigger
|
|
3. View linked accounts
|
|
|
|
---
|
|
|
|
**Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️
|
|
|
|
**Implementation Status:** READY TO BUILD
|
|
**Next Step:** Create Task #90 - Arbiter 2.x Implementation
|