Files
browser/platform_web/schemas/gameClientBridge.ts
T

309 lines
13 KiB
TypeScript

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", "unknown"]);
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled", "unknown"]);
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),
features: array(record.features ?? [], "features").map(parseFeature)
};
}
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"),
handlerTypes: stringArray(record.handlerTypes ?? [], "profile.handlerTypes"),
eventProducerTypes: stringArray(record.eventProducerTypes ?? [], "profile.eventProducerTypes")
};
}
function parseFeature(value: unknown): { key: string; available: boolean; reason?: string } {
const record = safeObject(value, "Game Client Bridge feature");
return { key: string(record.key, "feature.key"), available: boolean(record.available, "feature.available"), reason: optionalString(record.reason, "feature.reason") };
}
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;
}