Add SCUM Source RCON transport

This commit is contained in:
npc0-hue
2026-07-23 10:55:31 +08:00
parent ec96c22e4c
commit 742f96ea02
33 changed files with 1297 additions and 19 deletions
+6
View File
@@ -90,6 +90,8 @@ import type {
ServerLifecycleResponse,
ServerConfigWriteApprovalRequest,
ServerConfigWriteDispatchResponse,
SourceRCONCommandRequest,
SourceRCONCommandResponse,
ServerInstanceListResponse,
ServerDeletionRequest,
ServerInstanceUpdateRequest,
@@ -565,6 +567,10 @@ export class PlatformApiClient {
return this.request<RemoteAdapterResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/remote-adapters`, { method: "POST", body: request });
}
async sendSourceRCONCommand(serverInstanceId: string, request: SourceRCONCommandRequest): Promise<SourceRCONCommandResponse> {
return this.request<SourceRCONCommandResponse>(`/server-instances/${encodeURIComponent(serverInstanceId)}/rcon/commands`, { method: "POST", body: request });
}
async getServerConfig(id: string): Promise<ServerConfigResponse> {
return this.request<ServerConfigResponse>(`/server-instances/${encodeURIComponent(id)}/config`);
}
+44
View File
@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { PlatformApiClient } from "./client";
import type { SourceRCONCommandRequest } from "./types";
describe("PlatformApiClient Source RCON command dispatch", () => {
afterEach(() => vi.unstubAllGlobals());
it("posts the typed server-scoped request and projects only safe queue state", async () => {
const request: SourceRCONCommandRequest = {
kind: "chat",
chatType: 4,
message: "Maintenance complete",
targetSteamId: "76561198000000001",
idempotencyKey: "web:source-rcon:chat:server-1:12"
};
const calls: Array<{ url: string; method: string; body?: unknown; authorization: string | null }> = [];
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
calls.push({
url: String(input),
method: init?.method ?? "GET",
body: init?.body ? JSON.parse(String(init.body)) : undefined,
authorization: new Headers(init?.headers).get("Authorization")
});
return new Response(JSON.stringify({ jobId: "job-source-rcon", serverInstanceId: "server-1", status: "queued", message: "SCUM RCON command queued" }), {
status: 202,
headers: { "Content-Type": "application/json" }
});
}));
const response = await new PlatformApiClient("/api/v1", () => "operator-session").sendSourceRCONCommand("server/1", request);
expect(calls).toEqual([{
url: "/api/v1/server-instances/server%2F1/rcon/commands",
method: "POST",
body: request,
authorization: "Bearer operator-session"
}]);
expect(response).toEqual({ jobId: "job-source-rcon", serverInstanceId: "server-1", status: "queued", message: "SCUM RCON command queued" });
for (const forbidden of ["command", "password", "host", "configRef", "response"]) {
expect(Object.keys(response)).not.toContain(forbidden);
}
});
});
+20
View File
@@ -485,6 +485,26 @@ export interface ServerLifecycleResponse {
job: JobResponse;
}
export type SourceRCONCommandKind = "chat" | "command";
export interface SourceRCONCommandRequest {
kind: SourceRCONCommandKind;
chatType?: number;
message?: string;
targetSteamId?: string;
command?: string;
idempotencyKey: string;
}
// The Platform response intentionally excludes the command, local listener
// details, credentials, and Source RCON response body.
export interface SourceRCONCommandResponse {
jobId: string;
serverInstanceId: string;
status: JobState;
message: string;
}
export interface RunCapacityResponse {
maxJobs: number;
runningJobs: number;