import type { GameClientBridgeApprovalState, GameClientBridgeCancelResponse, GameClientBridgeCommandCancellationResponse, GameClientBridgeCommandListResponse, GameClientBridgeCommandResponse, GameClientBridgeCommandResultResponse, GameClientBridgeCommandState, GameClientBridgeJsonObject, GameClientBridgeJsonValue, GameClientBridgeProfileDeclarationResponse, GameClientBridgeResultStatus, GameClientBridgeSnapshotListResponse, GameClientBridgeSnapshotResponse, GameClientBridgeStatusResponse } from "../api/types"; const commandStates = new Set(["pending", "claimed", "succeeded", "failed", "cancelled", "expired"]); const approvalStates = new Set(["not_required", "pending", "approved", "rejected"]); const resultStatuses = new Set(["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), 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 { 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 { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`); return value as Record; } 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(source: Record, target: T, key: keyof T): void { const parsed = optionalString(source[key as string], String(key)); if (parsed) (target as Record)[key] = parsed; }