功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+4 -1
View File
@@ -38,10 +38,13 @@ describe("ai provider form schemas", () => {
...emptyAiProviderForm(),
id: "ai.openai",
apiKeyRef: "",
apiKeyConfigured: true
apiKeyConfigured: true,
baseUrl: "",
baseUrlConfigured: true
});
expect(request.apiKeyRef).toBe("");
expect(request.baseUrl).toBe("");
expect(JSON.stringify(request)).not.toContain("secret://providers/openai");
});
});
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { parseSafeClientManagerLifecycle, parseSafeClientManagerLifecycleList } from "./clientManagerLifecycle";
export const safeClientManagerLifecycleFixture = {
id: "client-manager-installation-1",
serverInstanceId: "server-1",
pluginId: "game.scum",
profileKey: "scum-client-manager",
targetOs: "windows",
targetArch: "amd64",
status: "online",
phase: "component heartbeat healthy",
desiredVersion: "2.0.0",
activeVersion: "2.0.0",
previousVersion: "1.0.0",
desiredRevision: "rev-2",
activeRevision: "rev-2",
previousRevision: "rev-1",
desiredArtifactId: "artifact-2",
activeArtifactId: "artifact-2",
previousArtifactId: "artifact-1",
keyGeneration: 3,
deploymentGeneration: 4,
currentJobId: "job-update-1",
lastSuccessfulJobId: "job-deploy-1",
lastOperation: "update",
health: "healthy",
healthReason: "component heartbeat healthy",
lastSeenAt: "2026-07-18T08:00:00Z",
retryable: false,
requiresRedeploy: false,
updatedAt: "2026-07-18T08:00:00Z",
distribution: { id: "distribution-2", artifactId: "artifact-2", sourceRevision: "rev-2", targetOs: "windows", targetArch: "amd64", checksum: `sha256:${"a".repeat(64)}`, keyGeneration: 3, status: "available" },
job: { id: "job-update-1", state: "running", progress: { percent: 65, message: "health confirmation" }, attempt: 1, createdAt: "2026-07-18T07:59:00Z", updatedAt: "2026-07-18T08:00:00Z" },
actions: [
{ operation: "start", available: false, reason: "already online" },
{ operation: "stop", available: true },
{ operation: "rollback", available: true }
]
} as const;
describe("Client Manager lifecycle schema", () => {
it("preserves safe lifecycle, job progress, versions and action availability", () => {
const parsed = parseSafeClientManagerLifecycle(safeClientManagerLifecycleFixture);
expect(parsed).toMatchObject({ status: "online", health: "healthy", activeVersion: "2.0.0", previousVersion: "1.0.0", job: { state: "running", progress: { percent: 65 } } });
expect(parseSafeClientManagerLifecycleList({ items: [safeClientManagerLifecycleFixture], count: 1 })).toMatchObject({ count: 1, items: [{ profileKey: "scum-client-manager" }] });
});
it.each([
{ runEndpointId: "run-private" },
{ pid: 4124 },
{ secretRef: "redacted" },
{ healthReason: "/Users/operator/client-manager" },
{ healthReason: "unix://private.sock" }
])("rejects machine and credential projection %#", (unsafe) => {
expect(() => parseSafeClientManagerLifecycle({ ...safeClientManagerLifecycleFixture, ...unsafe })).toThrow(/forbidden|sensitive/i);
});
});
@@ -0,0 +1,117 @@
import type {
ClientManagerInstallationListResponse,
ClientManagerInstallationResponse,
ClientManagerLifecycleActionResponse,
ClientManagerLifecycleOperation,
ClientManagerLifecycleStatus,
JobState
} from "../api/types";
const lifecycleStatuses = new Set<ClientManagerLifecycleStatus>([
"requested", "building", "available", "deploying", "installed", "registering", "online", "degraded", "offline", "updating", "rolling_back", "stopping", "uninstalled", "failed"
]);
const lifecycleOperations = new Set<ClientManagerLifecycleOperation>(["deploy", "start", "stop", "restart", "status", "update", "rollback", "uninstall"]);
const jobStates = new Set<JobState>(["queued", "accepted", "running", "retrying", "succeeded", "failed", "cancelled"]);
const forbiddenKeys = new Set(["key", "token", "secretref", "secretvalue", "hostpath", "pid", "socket", "credential", "dsn", "password", "runendpointid"]);
const forbiddenFragments = ["secret://", "/users/", "/var/run/", "bearer ", "password=", "unix://", "tcp://", "mysql://", "sqlite://", "rcon://"];
export function parseSafeClientManagerLifecycleList(value: unknown): ClientManagerInstallationListResponse {
const record = object(value, "Client Manager lifecycle list");
rejectSensitiveProjection(record);
const items = array(record.items, "items").map(parseSafeClientManagerLifecycle);
const count = number(record.count, "count");
return { items, count };
}
export function parseSafeClientManagerLifecycle(value: unknown): ClientManagerInstallationResponse {
const record = object(value, "Client Manager lifecycle");
rejectSensitiveProjection(record);
const status = string(record.status, "status") as ClientManagerLifecycleStatus;
if (!lifecycleStatuses.has(status)) throw new Error("Client Manager lifecycle status is invalid");
const actions = array(record.actions, "actions").map(parseAction);
const result: ClientManagerInstallationResponse = {
id: string(record.id, "id"),
serverInstanceId: string(record.serverInstanceId, "serverInstanceId"),
pluginId: string(record.pluginId, "pluginId"),
profileKey: string(record.profileKey, "profileKey"),
targetOs: string(record.targetOs, "targetOs"),
targetArch: string(record.targetArch, "targetArch"),
status,
phase: string(record.phase, "phase"),
keyGeneration: number(record.keyGeneration, "keyGeneration"),
deploymentGeneration: number(record.deploymentGeneration, "deploymentGeneration"),
health: health(record.health),
retryable: boolean(record.retryable, "retryable"),
requiresRedeploy: boolean(record.requiresRedeploy, "requiresRedeploy"),
updatedAt: string(record.updatedAt, "updatedAt"),
actions
};
copyOptionalStrings(record, result, ["desiredVersion", "activeVersion", "previousVersion", "desiredRevision", "activeRevision", "previousRevision", "desiredArtifactId", "activeArtifactId", "previousArtifactId", "currentJobId", "lastSuccessfulJobId", "healthReason", "lastSeenAt", "installedAt", "uninstalledAt"]);
if (record.lastOperation !== undefined) {
const operation = string(record.lastOperation, "lastOperation") as ClientManagerLifecycleOperation;
if (!lifecycleOperations.has(operation)) throw new Error("Client Manager lifecycle operation is invalid");
result.lastOperation = operation;
}
if (record.distribution !== undefined) {
const distribution = object(record.distribution, "distribution");
result.distribution = {
id: string(distribution.id, "distribution.id"), artifactId: string(distribution.artifactId, "distribution.artifactId"), sourceRevision: string(distribution.sourceRevision, "distribution.sourceRevision"),
targetOs: string(distribution.targetOs, "distribution.targetOs"), targetArch: string(distribution.targetArch, "distribution.targetArch"), checksum: string(distribution.checksum, "distribution.checksum"),
keyGeneration: number(distribution.keyGeneration, "distribution.keyGeneration"), status: string(distribution.status, "distribution.status")
};
}
if (record.job !== undefined) {
const job = object(record.job, "job");
const state = string(job.state, "job.state") as JobState;
if (!jobStates.has(state)) throw new Error("Client Manager job state is invalid");
const progress = object(job.progress, "job.progress");
result.job = { id: string(job.id, "job.id"), state, progress: { percent: number(progress.percent, "job.progress.percent"), message: optionalString(progress.message) }, attempt: number(job.attempt, "job.attempt"), createdAt: string(job.createdAt, "job.createdAt"), updatedAt: string(job.updatedAt, "job.updatedAt") };
}
return result;
}
function parseAction(value: unknown): ClientManagerLifecycleActionResponse {
const action = object(value, "action");
const operation = string(action.operation, "action.operation") as ClientManagerLifecycleOperation;
if (!lifecycleOperations.has(operation)) throw new Error("Client Manager action is invalid");
return { operation, available: boolean(action.available, "action.available"), reason: optionalString(action.reason) };
}
function rejectSensitiveProjection(value: unknown, key = ""): void {
if (typeof value === "string") {
const normalized = value.toLowerCase();
if (forbiddenFragments.some((fragment) => normalized.includes(fragment))) throw new Error("Client Manager response contains sensitive machine data");
return;
}
if (Array.isArray(value)) {
value.forEach((item) => rejectSensitiveProjection(item, key));
return;
}
if (value && typeof value === "object") {
for (const [childKey, child] of Object.entries(value)) {
if (forbiddenKeys.has(childKey.toLowerCase())) throw new Error("Client Manager response contains a forbidden field");
rejectSensitiveProjection(child, childKey);
}
}
}
function copyOptionalStrings(source: Record<string, unknown>, target: ClientManagerInstallationResponse, keys: Array<keyof ClientManagerInstallationResponse>) {
for (const key of keys) {
const value = source[key];
if (typeof value === "string" && value !== "") (target as unknown as Record<string, unknown>)[key] = value;
}
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
return value as Record<string, unknown>;
}
function array(value: unknown, label: string): unknown[] { if (!Array.isArray(value)) throw new Error(`${label} must be an array`); return value; }
function string(value: unknown, label: string): string { if (typeof value !== "string" || value === "") throw new Error(`${label} must be a string`); return value; }
function optionalString(value: unknown): string | undefined { return typeof value === "string" && value !== "" ? value : undefined; }
function number(value: unknown, label: string): number { if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a number`); return value; }
function boolean(value: unknown, label: string): boolean { if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`); return value; }
function health(value: unknown): ClientManagerInstallationResponse["health"] {
if (value === "unknown" || value === "healthy" || value === "degraded" || value === "unhealthy" || value === "offline") return value;
throw new Error("Client Manager health is invalid");
}
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import {
parseSafeGameClientBridgeCommand,
parseSafeGameClientBridgeSnapshotList,
parseSafeGameClientBridgeStatus
} from "./gameClientBridge";
const now = "2026-07-20T08:00:00Z";
const safeCommand = {
id: "command-1",
serverInstanceId: "server-1",
pluginId: "game.scum",
profileKey: "scum-client",
commandType: "scum.player.lookup",
priority: 10,
state: "succeeded",
approvalState: "not_required",
requesterId: "user-1",
result: { status: "succeeded", summary: "player found", payload: { found: true }, completedAt: now },
auditReferences: ["audit-1"],
expiresAt: now,
createdAt: now,
updatedAt: now,
completedAt: now
};
const safeSnapshotList = {
items: [{
id: "snapshot-1",
serverInstanceId: "server-1",
pluginId: "game.scum",
profileKey: "scum-client",
type: "scum.players",
schemaVersion: "1",
streamKey: "current",
sequence: 1,
observedAt: now,
payload: { players: [{ playerId: "player-1" }] },
retention: { keepForSeconds: 3600, maxRecords: 24 },
createdAt: now,
expiresAt: now
}],
count: 1
};
describe("Game Client Bridge safe projection schema", () => {
it("preserves declarations, approval, result, retention and typed snapshot payloads", () => {
expect(parseSafeGameClientBridgeStatus({
serverInstanceId: "server-1",
pluginId: "game.scum",
available: true,
profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: true, commandTypes: ["scum.player.lookup"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
})).toMatchObject({ available: true, profiles: [{ queryTemplateKeys: ["scum.player.search"] }] });
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ approvalState: "not_required", result: { payload: { found: true } } });
expect(parseSafeGameClientBridgeSnapshotList(safeSnapshotList)).toMatchObject({ count: 1, items: [{ retention: { maxRecords: 24 } }] });
expect(parseSafeGameClientBridgeSnapshotList({
...safeSnapshotList,
items: [{ ...safeSnapshotList.items[0], payload: { sessions: [{ sessionId: "game-session-1", playerId: "player-1" }] } }]
})).toMatchObject({ items: [{ payload: { sessions: [{ sessionId: "game-session-1" }] } }] });
});
it.each([
{ sessionToken: "component-session-material" },
{ componentSession: "component-session-material" },
{ componentKey: "raw-component-key" },
{ sourceSessionId: "component-session-1" },
{ result: { ...safeCommand.result, payload: { dsn: "sqlite:///srv/scum/SCUM.db" } } },
{ result: { ...safeCommand.result, payload: { endpoint: "tcp://127.0.0.1:9999" } } },
{ result: { ...safeCommand.result, payload: { output: "/Users/operator/scum/config.yaml" } } }
])("rejects forbidden command projection %#", (unsafe) => {
expect(() => parseSafeGameClientBridgeCommand({ ...safeCommand, ...unsafe })).toThrow(/forbidden|sensitive/i);
});
it("rejects credentials nested inside snapshot payloads", () => {
const unsafe = structuredClone(safeSnapshotList);
unsafe.items[0].payload = { players: [{ playerId: "player-1", credential: "raw-password" }] } as unknown as typeof unsafe.items[0]["payload"];
expect(() => parseSafeGameClientBridgeSnapshotList(unsafe)).toThrow(/forbidden/i);
});
});
+300
View File
@@ -0,0 +1,300 @@
import type {
GameClientBridgeApprovalState,
GameClientBridgeCancelResponse,
GameClientBridgeCommandCancellationResponse,
GameClientBridgeCommandListResponse,
GameClientBridgeCommandResponse,
GameClientBridgeCommandResultResponse,
GameClientBridgeCommandState,
GameClientBridgeJsonObject,
GameClientBridgeJsonValue,
GameClientBridgeProfileDeclarationResponse,
GameClientBridgeResultStatus,
GameClientBridgeSnapshotListResponse,
GameClientBridgeSnapshotResponse,
GameClientBridgeStatusResponse
} from "../api/types";
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired"]);
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled"]);
const forbiddenKeys = new Set([
"apikey",
"accesskey",
"accesskeyid",
"claimlease",
"componentkey",
"componentsession",
"componentsessionid",
"credential",
"credentials",
"deploymentgeneration",
"dsn",
"fencingtoken",
"hostpath",
"installationid",
"keygeneration",
"leaseexpiresat",
"password",
"privatekey",
"rawcredential",
"runendpoint",
"runendpointurl",
"secret",
"secretref",
"secretvalue",
"sessiontoken",
"socket",
"sourcesessionid",
"storagecredential",
"token"
]);
const forbiddenFragments = [
"bearer ",
"password=",
"secret://",
"unix://",
"tcp://",
"mysql://",
"postgres://",
"sqlite://",
"rcon://"
];
const forbiddenHostPath = /(?:^|[\s"'])(?:\/[Uu]sers\/|\/home\/|\/root\/|\/var\/|\/etc\/|\/opt\/|[a-z]:[\\/]|\\\\[^\\]+\\)/;
export function parseSafeGameClientBridgeStatus(value: unknown): GameClientBridgeStatusResponse {
const record = safeObject(value, "Game Client Bridge status");
return {
serverInstanceId: string(record.serverInstanceId, "serverInstanceId"),
pluginId: string(record.pluginId, "pluginId"),
available: boolean(record.available, "available"),
reason: optionalString(record.reason, "reason"),
profiles: array(record.profiles, "profiles").map(parseProfile)
};
}
export function parseSafeGameClientBridgeCommand(value: unknown): GameClientBridgeCommandResponse {
const record = safeObject(value, "Game Client Bridge command");
const result: GameClientBridgeCommandResponse = {
id: string(record.id, "id"),
serverInstanceId: string(record.serverInstanceId, "serverInstanceId"),
pluginId: string(record.pluginId, "pluginId"),
profileKey: string(record.profileKey, "profileKey"),
commandType: string(record.commandType, "commandType"),
priority: number(record.priority, "priority"),
state: commandState(record.state),
approvalState: approvalState(record.approvalState),
expiresAt: string(record.expiresAt, "expiresAt"),
createdAt: string(record.createdAt, "createdAt"),
updatedAt: string(record.updatedAt, "updatedAt")
};
copyOptionalString(record, result, "requesterId");
copyOptionalString(record, result, "resultSummary");
copyOptionalString(record, result, "completedAt");
const auditReferences = optionalStringArray(record.auditReferences, "auditReferences");
if (auditReferences) result.auditReferences = auditReferences;
if (record.result !== undefined) result.result = parseResult(record.result);
if (record.cancellation !== undefined) result.cancellation = parseCancellation(record.cancellation);
return result;
}
export function parseSafeGameClientBridgeCommandList(value: unknown): GameClientBridgeCommandListResponse {
const record = safeObject(value, "Game Client Bridge command list");
return {
items: array(record.items, "items").map(parseSafeGameClientBridgeCommand),
count: nonNegativeInteger(record.count, "count")
};
}
export function parseSafeGameClientBridgeCancellation(value: unknown): GameClientBridgeCancelResponse {
const record = safeObject(value, "Game Client Bridge cancellation");
const result: GameClientBridgeCancelResponse = {
commandId: string(record.commandId, "commandId"),
state: commandState(record.state),
cancellation: parseCancellation(record.cancellation),
updatedAt: string(record.updatedAt, "updatedAt")
};
const auditReferences = optionalStringArray(record.auditReferences, "auditReferences");
if (auditReferences) result.auditReferences = auditReferences;
return result;
}
export function parseSafeGameClientBridgeSnapshotList(value: unknown): GameClientBridgeSnapshotListResponse {
const record = safeObject(value, "Game Client Bridge snapshot list");
return {
items: array(record.items, "items").map(parseSnapshot),
count: nonNegativeInteger(record.count, "count")
};
}
function parseProfile(value: unknown): GameClientBridgeProfileDeclarationResponse {
const record = safeObject(value, "Game Client Bridge profile");
return {
pluginId: string(record.pluginId, "profile.pluginId"),
profileKey: string(record.profileKey, "profile.profileKey"),
available: boolean(record.available, "profile.available"),
reason: optionalString(record.reason, "profile.reason"),
commandTypes: stringArray(record.commandTypes, "profile.commandTypes"),
snapshotTypes: stringArray(record.snapshotTypes, "profile.snapshotTypes"),
queryTemplateKeys: stringArray(record.queryTemplateKeys, "profile.queryTemplateKeys")
};
}
function parseResult(value: unknown): GameClientBridgeCommandResultResponse {
const record = safeObject(value, "Game Client Bridge command result");
const result: GameClientBridgeCommandResultResponse = {
status: resultStatus(record.status),
completedAt: string(record.completedAt, "result.completedAt")
};
const summary = optionalString(record.summary, "result.summary");
if (summary) result.summary = summary;
if (record.payload !== undefined) result.payload = jsonObject(record.payload, "result.payload");
return result;
}
function parseCancellation(value: unknown): GameClientBridgeCommandCancellationResponse {
const record = safeObject(value, "Game Client Bridge command cancellation");
const result: GameClientBridgeCommandCancellationResponse = {
cancelledAt: string(record.cancelledAt, "cancellation.cancelledAt")
};
const requestedBy = optionalString(record.requestedBy, "cancellation.requestedBy");
const reason = optionalString(record.reason, "cancellation.reason");
if (requestedBy) result.requestedBy = requestedBy;
if (reason) result.reason = reason;
return result;
}
function parseSnapshot(value: unknown): GameClientBridgeSnapshotResponse {
const record = safeObject(value, "Game Client Bridge snapshot");
const retention = safeObject(record.retention, "snapshot.retention");
const result: GameClientBridgeSnapshotResponse = {
id: string(record.id, "snapshot.id"),
serverInstanceId: string(record.serverInstanceId, "snapshot.serverInstanceId"),
pluginId: string(record.pluginId, "snapshot.pluginId"),
profileKey: string(record.profileKey, "snapshot.profileKey"),
type: string(record.type, "snapshot.type"),
schemaVersion: string(record.schemaVersion, "snapshot.schemaVersion"),
streamKey: string(record.streamKey, "snapshot.streamKey"),
sequence: nonNegativeInteger(record.sequence, "snapshot.sequence"),
observedAt: string(record.observedAt, "snapshot.observedAt"),
payload: jsonObject(record.payload, "snapshot.payload"),
retention: {
keepForSeconds: nonNegativeInteger(retention.keepForSeconds, "snapshot.retention.keepForSeconds")
},
createdAt: string(record.createdAt, "snapshot.createdAt"),
expiresAt: string(record.expiresAt, "snapshot.expiresAt")
};
if (retention.maxRecords !== undefined) result.retention.maxRecords = nonNegativeInteger(retention.maxRecords, "snapshot.retention.maxRecords");
const auditReferences = optionalStringArray(record.auditReferences, "snapshot.auditReferences");
if (auditReferences) result.auditReferences = auditReferences;
return result;
}
function safeObject(value: unknown, label: string): Record<string, unknown> {
const record = object(value, label);
rejectSensitiveProjection(record);
return record;
}
function rejectSensitiveProjection(value: unknown): void {
if (typeof value === "string") {
const normalized = value.toLowerCase();
if (forbiddenFragments.some((fragment) => normalized.includes(fragment)) || forbiddenHostPath.test(value)) {
throw new Error("Game Client Bridge response contains sensitive connection or host data");
}
return;
}
if (Array.isArray(value)) {
value.forEach(rejectSensitiveProjection);
return;
}
if (value && typeof value === "object") {
for (const [key, child] of Object.entries(value)) {
if (forbiddenKeys.has(key.toLowerCase().replace(/[^a-z0-9]/g, ""))) {
throw new Error("Game Client Bridge response contains a forbidden field");
}
rejectSensitiveProjection(child);
}
}
}
function jsonObject(value: unknown, label: string): GameClientBridgeJsonObject {
const record = object(value, label);
return Object.fromEntries(Object.entries(record).map(([key, child]) => [key, jsonValue(child, `${label}.${key}`)]));
}
function jsonValue(value: unknown, label: string): GameClientBridgeJsonValue {
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
if (typeof value === "number" && Number.isFinite(value)) return value;
if (Array.isArray(value)) return value.map((child, index) => jsonValue(child, `${label}[${index}]`));
if (value && typeof value === "object") return jsonObject(value, label);
throw new Error(`${label} must be JSON-compatible`);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
return value as Record<string, unknown>;
}
function array(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
return value;
}
function string(value: unknown, label: string): string {
if (typeof value !== "string" || value === "") throw new Error(`${label} must be a string`);
return value;
}
function optionalString(value: unknown, label: string): string | undefined {
if (value === undefined || value === "") return undefined;
return string(value, label);
}
function number(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a number`);
return value;
}
function nonNegativeInteger(value: unknown, label: string): number {
const parsed = number(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative safe integer`);
return parsed;
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
return value;
}
function stringArray(value: unknown, label: string): string[] {
return array(value, label).map((item, index) => string(item, `${label}[${index}]`));
}
function optionalStringArray(value: unknown, label: string): string[] | undefined {
if (value === undefined) return undefined;
return stringArray(value, label);
}
function commandState(value: unknown): GameClientBridgeCommandState {
const parsed = string(value, "state") as GameClientBridgeCommandState;
if (!commandStates.has(parsed)) throw new Error("Game Client Bridge command state is invalid");
return parsed;
}
function approvalState(value: unknown): GameClientBridgeApprovalState {
const parsed = string(value, "approvalState") as GameClientBridgeApprovalState;
if (!approvalStates.has(parsed)) throw new Error("Game Client Bridge approval state is invalid");
return parsed;
}
function resultStatus(value: unknown): GameClientBridgeResultStatus {
const parsed = string(value, "result.status") as GameClientBridgeResultStatus;
if (!resultStatuses.has(parsed)) throw new Error("Game Client Bridge result status is invalid");
return parsed;
}
function copyOptionalString<T extends object>(source: Record<string, unknown>, target: T, key: keyof T): void {
const parsed = optionalString(source[key as string], String(key));
if (parsed) (target as Record<keyof T, unknown>)[key] = parsed;
}
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import type { GameClientBridgeJsonObject, GameClientBridgeSnapshotResponse } from "../api/types";
import { projectScumOperationsSnapshots } from "./scumOperations";
const now = "2026-07-20T08:00:00Z";
function snapshot(type: string, payload: GameClientBridgeJsonObject, sequence = 1): GameClientBridgeSnapshotResponse {
return {
id: `${type}-${sequence}`,
serverInstanceId: "server-1",
pluginId: "game.scum",
profileKey: "scum-client",
type,
schemaVersion: "1",
streamKey: "current",
sequence,
observedAt: now,
payload,
retention: { keepForSeconds: 3600, maxRecords: 24 },
createdAt: now,
expiresAt: "2026-07-20T09:00:00Z"
};
}
describe("SCUM operations snapshot projection", () => {
it("projects companion, player, session, squad, vehicle and flag snapshots", () => {
const view = projectScumOperationsSnapshots([
snapshot("companion.health", { status: "online", version: "1.2.0", observedAt: now, latencyMs: 24, capabilities: ["game-client.bridge"] }),
snapshot("online.sessions", { observedAt: now, onlineCount: 1, sessions: [{ sessionId: "game-session-1", playerName: "Moonlight", startedAt: now }] }),
snapshot("players", { observedAt: now, players: [{ playerId: "player-1", playerName: "Moonlight", status: "online", squadId: "squad-1", pingMs: 33 }] }),
snapshot("squads", { observedAt: now, squads: [{ squadId: "squad-1", name: "Lunar", memberCount: 4, leaderPlayerId: "player-1" }] }),
snapshot("vehicles", { observedAt: now, vehicles: [{ vehicleId: "vehicle-1", vehicleType: "truck", status: "parked", ownerPlayerId: "player-1", fuelPercent: 70, healthPercent: 80 }] }),
snapshot("flags", { observedAt: now, flags: [{ flagId: "flag-1", status: "active", squadId: "squad-1", radiusMeters: 25 }] })
]);
expect(view).toMatchObject({
health: { status: "online", version: "1.2.0", latencyMs: 24 },
sessions: { total: 1, items: [{ sessionId: "game-session-1" }] },
players: { total: 1, items: [{ playerId: "player-1", squadId: "squad-1" }] },
squads: { total: 1, items: [{ memberCount: 4 }] },
vehicles: { total: 1, items: [{ fuelPercent: 70 }] },
flags: { total: 1, items: [{ radiusMeters: 25 }] }
});
});
it("uses the newest sequence and redacts sensitive-looking display strings", () => {
const view = projectScumOperationsSnapshots([
snapshot("players", { observedAt: now, players: [{ playerId: "player-old", playerName: "Old", status: "offline" }] }, 1),
{ ...snapshot("players", { observedAt: now, players: [{ playerId: "player-new", playerName: "token=raw-secret /Users/operator/file", status: "online" }] }, 2), observedAt: now }
]);
expect(view.players.items[0]).toMatchObject({ playerId: "player-new", status: "online" });
expect(view.players.items[0]?.playerName).not.toContain("raw-secret");
expect(view.players.items[0]?.playerName).not.toContain("/Users/");
});
});
+148
View File
@@ -0,0 +1,148 @@
import type { GameClientBridgeJsonObject, GameClientBridgeSnapshotResponse } from "../api/types";
import type {
ScumCompanionHealthView,
ScumFlagView,
ScumOperationsSnapshotView,
ScumPlayerView,
ScumSessionView,
ScumSnapshotCollection,
ScumSquadView,
ScumVehicleView
} from "../contracts/scumOperations";
import { safeDiagnosticText } from "../utils/safeDiagnosticText";
const maxVisibleItems = 50;
export function projectScumOperationsSnapshots(snapshots: GameClientBridgeSnapshotResponse[]): ScumOperationsSnapshotView {
return {
health: projectHealth(latestPayload(snapshots, "companion.health")),
sessions: projectCollection(latestSnapshot(snapshots, "online.sessions"), "sessions", projectSession),
players: projectCollection(latestSnapshot(snapshots, "players"), "players", projectPlayer),
squads: projectCollection(latestSnapshot(snapshots, "squads"), "squads", projectSquad),
vehicles: projectCollection(latestSnapshot(snapshots, "vehicles"), "vehicles", projectVehicle),
flags: projectCollection(latestSnapshot(snapshots, "flags"), "flags", projectFlag)
};
}
function latestSnapshot(snapshots: GameClientBridgeSnapshotResponse[], type: string): GameClientBridgeSnapshotResponse | undefined {
return snapshots
.filter((snapshot) => snapshot.type === type)
.sort((left, right) => right.observedAt.localeCompare(left.observedAt) || right.sequence - left.sequence)[0];
}
function latestPayload(snapshots: GameClientBridgeSnapshotResponse[], type: string): GameClientBridgeJsonObject | undefined {
return latestSnapshot(snapshots, type)?.payload;
}
function projectHealth(payload: GameClientBridgeJsonObject | undefined): ScumCompanionHealthView | undefined {
if (!payload) return undefined;
const status = readText(payload.status, 20);
return {
status: status === "online" || status === "degraded" || status === "offline" ? status : "unknown",
version: readOptionalText(payload.version, 40),
observedAt: readOptionalText(payload.observedAt, 64),
latencyMs: readOptionalNumber(payload.latencyMs, 0, 30000),
capabilities: readArray(payload.capabilities).map((value) => readText(value, 80)).filter(Boolean).slice(0, 16)
};
}
function projectCollection<T>(
snapshot: GameClientBridgeSnapshotResponse | undefined,
key: string,
project: (value: unknown) => T | null
): ScumSnapshotCollection<T> {
const values = snapshot ? readArray(snapshot.payload[key]) : [];
return {
observedAt: snapshot?.observedAt,
total: values.length,
items: values.slice(0, maxVisibleItems).map(project).filter((value): value is T => value !== null)
};
}
function projectSession(value: unknown): ScumSessionView | null {
const record = readObject(value);
const sessionId = readText(record?.sessionId, 120);
const playerName = readText(record?.playerName, 80);
return sessionId && playerName ? { sessionId, playerName, startedAt: readOptionalText(record?.startedAt, 64) } : null;
}
function projectPlayer(value: unknown): ScumPlayerView | null {
const record = readObject(value);
const playerId = readText(record?.playerId, 96);
const playerName = readText(record?.playerName, 80);
const status = readText(record?.status, 20);
return playerId && playerName && status ? {
playerId,
playerName,
status,
squadId: readOptionalText(record?.squadId, 96),
pingMs: readOptionalNumber(record?.pingMs, 0, 10000),
lastSeenAt: readOptionalText(record?.lastSeenAt, 64)
} : null;
}
function projectSquad(value: unknown): ScumSquadView | null {
const record = readObject(value);
const squadId = readText(record?.squadId, 96);
const name = readText(record?.name, 80);
const memberCount = readOptionalNumber(record?.memberCount, 0, 64);
return squadId && name && memberCount !== undefined ? {
squadId,
name,
memberCount,
leaderPlayerId: readOptionalText(record?.leaderPlayerId, 96),
lastActiveAt: readOptionalText(record?.lastActiveAt, 64)
} : null;
}
function projectVehicle(value: unknown): ScumVehicleView | null {
const record = readObject(value);
const vehicleId = readText(record?.vehicleId, 96);
const vehicleType = readText(record?.vehicleType, 80);
const status = readText(record?.status, 20);
return vehicleId && vehicleType && status ? {
vehicleId,
vehicleType,
status,
ownerPlayerId: readOptionalText(record?.ownerPlayerId, 96),
squadId: readOptionalText(record?.squadId, 96),
fuelPercent: readOptionalNumber(record?.fuelPercent, 0, 100),
healthPercent: readOptionalNumber(record?.healthPercent, 0, 100),
lastSeenAt: readOptionalText(record?.lastSeenAt, 64)
} : null;
}
function projectFlag(value: unknown): ScumFlagView | null {
const record = readObject(value);
const flagId = readText(record?.flagId, 96);
const status = readText(record?.status, 20);
return flagId && status ? {
flagId,
status,
ownerPlayerId: readOptionalText(record?.ownerPlayerId, 96),
squadId: readOptionalText(record?.squadId, 96),
radiusMeters: readOptionalNumber(record?.radiusMeters, 0, 5000),
lastUpdatedAt: readOptionalText(record?.lastUpdatedAt, 64)
} : null;
}
function readObject(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
}
function readArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function readText(value: unknown, maxLength: number): string {
if (typeof value !== "string") return "";
return (safeDiagnosticText(value.slice(0, maxLength), "") ?? "").trim();
}
function readOptionalText(value: unknown, maxLength: number): string | undefined {
return readText(value, maxLength) || undefined;
}
function readOptionalNumber(value: unknown, minimum: number, maximum: number): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= minimum && value <= maximum ? value : undefined;
}
@@ -19,6 +19,7 @@ const plugin: GamePluginResponse = {
pages: [],
tags: [],
aiPurposes: [],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
status: "installed",
runtimeProfiles: {
discovery: [{ key: "root-check", kind: "file.exists", targetKey: "server-root", required: true }],