Persist Plutonium keys by server ID and add key management

This commit is contained in:
2026-09-17 15:54:10 -04:00
parent e4505a3be8
commit ae173517b6
10 changed files with 305 additions and 19 deletions
+24 -3
View File
@@ -2,6 +2,7 @@ import { mkdir, chmod, rm, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { config, destOfKind } from '../config.js';
import { db } from '../db/index.js';
import { getServerKey, saveServerKey } from './server-keys.js';
import { getServer } from './servers.js';
import { t5ZombieMapBySlug, type GameMode } from './map-aliases.js';
import {
@@ -26,6 +27,7 @@ const T5_ZOMBIE_CONFIG_FILE = config.configTargetFile;
const T5_ZOMBIE_GAME_ID = 't5sp';
export type CreateServerFailure =
| 'bad-request'
| 'unconfigured' // Komodo creds / target server missing
| 'unknown-map' // slug isn't in the catalog
| 'no-slot' // all lobby slots for this map are taken
@@ -45,7 +47,8 @@ export type DestroyServerResult =
export interface CreateServerInput {
gameMode: GameMode; // 'zombie' — only value supported today
mapSlug: string; // 'cosmodrome'
plutoKey: string; // required; never persisted
plutoKey?: string; // saved for this server ID when supplied
lobby?: number; // explicit stable identity; otherwise first available
createdBy?: string; // 'ui' by default
}
@@ -132,7 +135,15 @@ function buildSpec(args: {
};
}
let creating = false;
export async function createServer(input: CreateServerInput): Promise<CreateServerResult> {
if (creating) return { ok: false, kind: 'no-slot', message: 'Another server is being created. Please retry shortly.' };
creating = true;
try { return await createServerUnlocked(input); }
finally { creating = false; }
}
async function createServerUnlocked(input: CreateServerInput): Promise<CreateServerResult> {
if (input.gameMode !== 'zombie') {
return { ok: false, kind: 'unknown-map', message: `unsupported game mode: ${input.gameMode}` };
}
@@ -149,11 +160,17 @@ export async function createServer(input: CreateServerInput): Promise<CreateServ
}
const baseName = `t5_${input.gameMode}_${mapEntry.slug}`;
const lobby = nextLobbyNumber(baseName);
if (input.lobby !== undefined && (!Number.isInteger(input.lobby) || input.lobby < 1 || input.lobby > 20)) {
return { ok: false, kind: 'bad-request', message: 'Lobby must be an integer from 1 to 20' };
}
const lobby = input.lobby ?? nextLobbyNumber(baseName);
if (lobby === null) {
return { ok: false, kind: 'no-slot', message: `all lobby slots (1..20) for ${baseName} are taken` };
}
const name = `${baseName}_${lobby}`;
if (getServer(name)) return { ok: false, kind: 'no-slot', message: `${name} is already deployed` };
const plutoKey = input.plutoKey?.trim() || getServerKey(name);
if (!plutoKey) return { ok: false, kind: 'bad-request', message: `No saved key for ${name}. Enter a Plutonium server key.` };
const port = nextAvailablePort();
if (port === null) {
@@ -164,6 +181,10 @@ export async function createServer(input: CreateServerInput): Promise<CreateServ
};
}
// Save before external operations so a failed deployment can also be retried.
try { saveServerKey(name, plutoKey); }
catch (err) { return { ok: false, kind: 'bad-request', message: (err as Error).message }; }
const hostDir = `${config.serversRoot}/${name}`;
const initialConfig = getPromotedServerConfig('t5', mapEntry.canonical);
const mapConfig = getServerMapConfig('t5', mapEntry.canonical)!;
@@ -190,7 +211,7 @@ export async function createServer(input: CreateServerInput): Promise<CreateServ
}
// Step 2: Komodo CreateDeployment.
const spec = buildSpec({ name, port, hostDir, plutoKey: input.plutoKey });
const spec = buildSpec({ name, port, hostDir, plutoKey });
const createRes = await komodoCreateDeployment(spec);
if (!createRes.ok) {
await rm(hostDir, { recursive: true, force: true }).catch(() => {});