package companion import ( "context" "fmt" "sort" "strings" "unicode/utf8" ) // AuthorizedConfigPort is supplied by a version-bound Companion integration. // It exposes logical configuration values only: never a host path, connection // string, credential, arbitrary command, or direct database handle. type AuthorizedConfigPort interface { ReadConfig(context.Context) (map[string]string, error) ApplyConfigPatch(ctx context.Context, revision string, fields []ConfigFieldPatch) (map[string]string, error) } type ConfigFieldPatch struct { Key string Value string } const ( UE4SSReferenceRevision = "bae91527355f14faa63c1df65f742cc48594ba1b" UE4SSReferenceBuild = "3.0.1" fixedNotificationType = 4 ) // UE4SSNotificationPort is implemented only by a Companion-local, // platform-authorized transport for the pinned UE4SS build. It receives a // fixed typed notification, never a raw RCON command, credential, socket, or // host path. Its private audit text is not part of command result payloads. type UE4SSNotificationPort interface { SendPlayerNotification(context.Context, ue4SSPlayerNotification) (UE4SSNotificationReceipt, error) } type UE4SSNotificationReceipt struct{ Accepted bool } type ue4SSPlayerNotification struct { ServerID string RecipientSteamID string Message string chatType int protectedAuditCommand string } type VersionedAdapter struct { BoundServerID string ServerVersion string UE4SSBuild string UE4SSReferenceRevision string Config AuthorizedConfigPort Notification UE4SSNotificationPort DiagnosticsState map[string]string } func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) { if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil { return nil, fmt.Errorf("configuration adapter is unsupported") } fields, err := adapter.Config.ReadConfig(ctx) if err != nil { return nil, err } return map[string]any{"version": adapter.ServerVersion, "fields": redactConfigValues(fields)}, nil } func (adapter VersionedAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) { if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil { return nil, fmt.Errorf("configuration adapter is unsupported") } revision, _ := payload["revision"].(string) raw, _ := payload["fields"].([]any) fields := make([]ConfigFieldPatch, 0, len(raw)) for _, value := range raw { item, ok := value.(map[string]any) if !ok { return nil, fmt.Errorf("configuration patch payload is invalid") } key, keyOK := item["key"].(string) fieldValue, valueOK := item["value"].(string) if !keyOK || !valueOK || !supportedConfigKey(key) { return nil, fmt.Errorf("configuration patch field is unsupported") } fields = append(fields, ConfigFieldPatch{Key: key, Value: fieldValue}) } applied, err := adapter.Config.ApplyConfigPatch(ctx, revision, fields) if err != nil { return nil, err } return map[string]any{"version": adapter.ServerVersion, "appliedFields": redactConfigValues(applied)}, nil } func (adapter VersionedAdapter) Diagnostics(context.Context) (map[string]any, error) { state := map[string]any{"version": adapter.ServerVersion, "adapter": "version-bound", "configuration": supportedAdapterVersion(adapter.ServerVersion)} for key, value := range adapter.DiagnosticsState { if safeDiagnosticField(key, value) { state[key] = value } } return state, nil } func (VersionedAdapter) PatchGameState(context.Context, map[string]any) (map[string]any, error) { return nil, fmt.Errorf("state adapter is unsupported") } func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[string]any, error) { return nil, fmt.Errorf("reward adapter is unsupported") } func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) { if !adapter.supportsUE4SSNotification() || adapter.Notification == nil { return nil, fmt.Errorf("notification adapter is unsupported") } playerID, playerOK := payload["playerId"].(string) message, messageOK := payload["message"].(string) notification, err := newUE4SSPlayerNotification(adapter.BoundServerID, playerID, message) if !playerOK || !messageOK || err != nil { return nil, fmt.Errorf("notification payload is invalid") } receipt, err := adapter.Notification.SendPlayerNotification(ctx, notification) if err != nil { return nil, fmt.Errorf("notification transport failed") } if !receipt.Accepted { return map[string]any{"accepted": false, "message": "notification was not accepted"}, nil } return map[string]any{"accepted": true, "message": "notification accepted for online recipient"}, nil } func (adapter VersionedAdapter) supportsUE4SSNotification() bool { // The pinned source reflects SendChatLineToPlayer at runtime and fails // closed on a schema change, so no unverified SCUM-version mapping is // embedded here. The dispatcher still requires a discovered server version. return adapter.BoundServerID != "" && adapter.UE4SSBuild == UE4SSReferenceBuild && adapter.UE4SSReferenceRevision == UE4SSReferenceRevision } func newUE4SSPlayerNotification(serverID, playerID, message string) (ue4SSPlayerNotification, error) { if strings.TrimSpace(serverID) == "" || !steamID64(playerID) || !validNotificationMessage(message) { return ue4SSPlayerNotification{}, fmt.Errorf("invalid typed UE4SS notification") } return ue4SSPlayerNotification{ServerID: serverID, RecipientSteamID: playerID, Message: message, chatType: fixedNotificationType, protectedAuditCommand: "SendChat 4 \"" + escapeUE4SSChatMessage(message) + "\" " + playerID}, nil } func steamID64(value string) bool { if len(value) != 17 { return false } for _, character := range value { if character < '0' || character > '9' { return false } } return true } func validNotificationMessage(value string) bool { if value == "" || len(value) > 200 || !utf8.ValidString(value) { return false } for _, character := range value { if character < 0x20 || character == 0x7f { return false } } return true } func escapeUE4SSChatMessage(value string) string { return strings.NewReplacer("\\", "\\\\", "\"", "\\\"").Replace(value) } func supportedAdapterVersion(version string) bool { return version == "0.9.700.90357" } func supportedConfigKey(key string) bool { return map[string]bool{"ServerName": true, "GamePort": true, "QueryPort": true, "MaxPlayers": true, "WelcomeMessage": true}[key] } func redactConfigValues(values map[string]string) map[string]string { result := map[string]string{} keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { if supportedConfigKey(key) && safeDiagnosticField(key, values[key]) { result[key] = values[key] } } return result } func safeDiagnosticField(key, value string) bool { lowered := strings.ToLower(key + "=" + value) return !strings.Contains(lowered, "path") && !strings.Contains(lowered, "credential") && !strings.Contains(lowered, "password") && !strings.Contains(lowered, "bearer ") && !strings.Contains(lowered, "rcon") && !strings.Contains(lowered, "sql") && !strings.Contains(lowered, "://") }