feat(scum-companion): validate typed adapters

This commit is contained in:
npc0-hue
2026-07-29 10:50:32 +08:00
parent 7155e755f8
commit acf4e4a8f0
5 changed files with 261 additions and 6 deletions
@@ -0,0 +1,103 @@
package companion
import (
"context"
"fmt"
"sort"
"strings"
)
// 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
}
type VersionedAdapter struct {
ServerVersion string
Config AuthorizedConfigPort
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 (VersionedAdapter) NotifyPlayer(context.Context, map[string]any) (map[string]any, error) {
return nil, fmt.Errorf("notification adapter is unsupported")
}
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, "://")
}