package companion import ( "context" "fmt" "strings" "sync" "time" ) // SafeAdapter is intentionally narrow: it receives typed values only and has // no raw RCON, SQL, host-path, credential, or shell access. type SafeAdapter interface { ReadConfiguration(context.Context) (map[string]any, error) PatchConfiguration(context.Context, map[string]any) (map[string]any, error) Diagnostics(context.Context) (map[string]any, error) PatchGameState(context.Context, map[string]any) (map[string]any, error) DeliverReward(context.Context, map[string]any) (map[string]any, error) NotifyPlayer(context.Context, map[string]any) (map[string]any, error) SpawnVehicle(context.Context, map[string]any) (map[string]any, error) } type HandlerAvailability struct { BoundServerID string ServerVersion string Capabilities map[string]bool Approved bool } type CommandHandler func(context.Context, map[string]any) (map[string]any, error) type HandlerRegistry struct { availability HandlerAvailability handlers map[string]CommandHandler mu sync.Mutex completed map[string]CommandResult } func NewHandlerRegistry(availability HandlerAvailability, adapter SafeAdapter) *HandlerRegistry { registry := &HandlerRegistry{availability: availability, handlers: map[string]CommandHandler{}, completed: map[string]CommandResult{}} if adapter == nil { return registry } registry.handlers["config.read"] = func(ctx context.Context, _ map[string]any) (map[string]any, error) { return adapter.ReadConfiguration(ctx) } registry.handlers["config.patch"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) { return adapter.PatchConfiguration(ctx, payload) } registry.handlers["companion.diagnostics"] = func(ctx context.Context, _ map[string]any) (map[string]any, error) { return adapter.Diagnostics(ctx) } registry.handlers["game-state.patch"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) { return adapter.PatchGameState(ctx, payload) } registry.handlers["reward.deliver"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) { return adapter.DeliverReward(ctx, payload) } registry.handlers["player.notify"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) { return adapter.NotifyPlayer(ctx, payload) } registry.handlers["vehicle.spawn"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) { return adapter.SpawnVehicle(ctx, payload) } return registry } func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCommand) (CommandResult, error) { registry.mu.Lock() cached, done := registry.completed[command.ID] registry.mu.Unlock() if done { return cached, nil } if err := validateDeclaredCommandAt(command, time.Now); err != nil { return unsupportedResult("validation-failed"), nil } if strings.TrimSpace(registry.availability.BoundServerID) == "" || !registry.availability.Approved || !registry.availability.Capabilities[command.CommandType] { return unsupportedResult("unsupported"), nil } handler, exists := registry.handlers[command.CommandType] if !exists || strings.TrimSpace(registry.availability.ServerVersion) == "" { return unsupportedResult("unsupported"), nil } payload, err := handler(ctx, command.Payload) if err != nil { return CommandResult{Status: "failed", Summary: "typed adapter failed", Payload: map[string]any{"result": "failed"}}, nil } result := CommandResult{Status: "succeeded", Summary: "typed adapter completed", Payload: redactTypedPayload(payload)} registry.mu.Lock() registry.completed[command.ID] = result registry.mu.Unlock() return result, nil } func validateDeclaredCommandAt(command ClaimedCommand, now func() time.Time) error { if command.ID == "" || command.ProfileKey != ProfileKey || command.FencingToken == 0 || command.Payload == nil || command.LeaseExpiresAt.IsZero() || command.ExpiresAt.IsZero() || !now().Before(command.LeaseExpiresAt) || !now().Before(command.ExpiresAt) { return fmt.Errorf("invalid command") } for key, value := range command.Payload { if !safeCommandField(key, value) { return fmt.Errorf("unsafe payload") } } return validateCommandPayload(command.CommandType, command.Payload) } func validateCommandPayload(commandType string, payload map[string]any) error { require := func(keys ...string) error { for _, key := range keys { if _, ok := payload[key]; !ok { return fmt.Errorf("payload is incomplete") } } return nil } noUnknown := func(keys ...string) error { allowed := map[string]bool{} for _, key := range keys { allowed[key] = true } for key := range payload { if !allowed[key] { return fmt.Errorf("payload has unsupported field") } } return nil } switch commandType { case "config.read": return noUnknown() case "config.patch": if err := require("revision", "fields"); err != nil { return err } if err := noUnknown("revision", "fields"); err != nil { return err } _, revisionOK := payload["revision"].(string) fields, fieldsOK := payload["fields"].([]any) if !revisionOK || !fieldsOK || len(fields) == 0 || len(fields) > 32 { return fmt.Errorf("config patch payload is invalid") } return nil case "companion.diagnostics": if err := noUnknown("includeWindowState", "maxEntries"); err != nil { return err } if value, ok := payload["includeWindowState"]; ok { if _, valid := value.(bool); !valid { return fmt.Errorf("diagnostics payload is invalid") } } if value, ok := payload["maxEntries"]; ok { if !boundedDiagnosticsEntries(value) { return fmt.Errorf("diagnostics payload is invalid") } } return nil case "game-state.patch": if err := require("playerId", "gameVersion", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil { return err } if err := noUnknown("playerId", "gameVersion", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil { return err } version, ok := payload["gameVersion"].(string) changes, changesOK := payload["changes"].([]any) if !ok || version == "" || !changesOK || len(changes) == 0 || len(changes) > 8 { return fmt.Errorf("state patch payload is invalid") } return nil case "reward.deliver": if err := require("grantId", "playerId", "items"); err != nil { return err } if err := noUnknown("grantId", "playerId", "items"); err != nil { return err } _, grantOK := payload["grantId"].(string) _, playerOK := payload["playerId"].(string) items, itemsOK := payload["items"].([]any) if !grantOK || !playerOK || !itemsOK || len(items) == 0 || len(items) > 8 { return fmt.Errorf("reward payload is invalid") } return nil case "player.notify": if err := require("playerId", "message"); err != nil { return err } if err := noUnknown("playerId", "message"); err != nil { return err } _, playerOK := payload["playerId"].(string) message, messageOK := payload["message"].(string) if !playerOK || !messageOK || strings.TrimSpace(message) == "" || len(message) > 200 { return fmt.Errorf("notification payload is invalid") } return nil case "vehicle.spawn": if err := require("vehicleCode"); err != nil { return err } if err := noUnknown("vehicleCode"); err != nil { return err } vehicleCode, vehicleOK := payload["vehicleCode"].(string) if !vehicleOK || !supportedVehicleSpawnCode(vehicleCode) { return fmt.Errorf("vehicle spawn payload is invalid") } return nil default: return fmt.Errorf("command type is not declared") } } func safeCommandField(key string, value any) bool { lower := strings.ToLower(strings.TrimSpace(key)) if lower == "" || strings.Contains(lower, "path") || strings.Contains(lower, "credential") || strings.Contains(lower, "password") || strings.Contains(lower, "sql") || strings.Contains(lower, "rcon") || strings.Contains(lower, "command") { return false } if text, ok := value.(string); ok { compact := strings.ToLower(text) return !strings.Contains(compact, "bearer ") && !strings.Contains(compact, "password=") && !strings.Contains(compact, "select ") && !strings.Contains(compact, "/users/") } return true } func unsupportedResult(code string) CommandResult { return CommandResult{Status: "failed", Summary: "typed operation unavailable", Payload: map[string]any{"result": code}} } func redactTypedPayload(payload map[string]any) map[string]any { result := map[string]any{} for key, value := range payload { if safeCommandField(key, value) { result[key] = value } } return result } type CommandGateway interface { ClaimCommands(context.Context, int) ([]ClaimedCommand, error) AckCommand(context.Context, string, uint64) (CommandAck, error) CompleteCommand(context.Context, string, uint64, CommandResult) (CompletedCommand, error) } type Dispatcher struct { Client CommandGateway Registry *HandlerRegistry PollLimit int Backoff time.Duration Now func() time.Time } func (dispatcher Dispatcher) DispatchOnce(ctx context.Context) error { if dispatcher.Client == nil || dispatcher.Registry == nil { return fmt.Errorf("dispatcher is not configured") } limit := dispatcher.PollLimit if limit == 0 { limit = 10 } if limit < 1 || limit > 50 { return fmt.Errorf("dispatcher poll limit is invalid") } commands, err := dispatcher.Client.ClaimCommands(ctx, limit) if err != nil { return err } for _, command := range commands { if err := validateDeclaredCommandAt(command, dispatcher.clock()); err != nil { if completeErr := dispatcher.completeValidationFailure(ctx, command); completeErr != nil { return completeErr } continue } if _, err = dispatcher.Client.AckCommand(ctx, command.ID, command.FencingToken); err != nil { return err } result, executionErr := dispatcher.Registry.Execute(ctx, command) if executionErr != nil { result = CommandResult{Status: "failed", Summary: "typed adapter failed", Payload: map[string]any{"result": "failed"}} } if _, err = dispatcher.Client.CompleteCommand(ctx, command.ID, command.FencingToken, result); err != nil { return err } } return nil } func (dispatcher Dispatcher) completeValidationFailure(ctx context.Context, command ClaimedCommand) error { if command.ID == "" || command.FencingToken == 0 { return fmt.Errorf("claimed command is invalid") } _, err := dispatcher.Client.CompleteCommand(ctx, command.ID, command.FencingToken, unsupportedResult("validation-failed")) return err } func (dispatcher Dispatcher) clock() func() time.Time { if dispatcher.Now != nil { return dispatcher.Now } return time.Now } func (dispatcher Dispatcher) Run(ctx context.Context) error { backoff := dispatcher.Backoff if backoff <= 0 { backoff = 2 * time.Second } for { if err := dispatcher.DispatchOnce(ctx); err != nil { select { case <-ctx.Done(): return ctx.Err() case <-time.After(backoff): continue } } select { case <-ctx.Done(): return ctx.Err() case <-time.After(backoff): } } } // Runtime keeps the registered companion alive with bounded heartbeat and // polling intervals. It owns no host connection or game credential; handlers // are the only route to a version-bound adapter. type RuntimeGateway interface { CommandGateway Register(context.Context) (Registration, error) Heartbeat(context.Context, HealthReport) (HeartbeatResult, error) } type Runtime struct { Client RuntimeGateway Dispatcher Dispatcher HeartbeatEvery time.Duration PollEvery time.Duration Backoff time.Duration Health func() HealthReport } func (runtime Runtime) Run(ctx context.Context) error { if runtime.Client == nil || runtime.Dispatcher.Registry == nil { return fmt.Errorf("runtime is not configured") } if _, err := runtime.Client.Register(ctx); err != nil { return err } heartbeatEvery, pollEvery := runtime.HeartbeatEvery, runtime.PollEvery if heartbeatEvery < 5*time.Second { heartbeatEvery = 30 * time.Second } if pollEvery < time.Second { pollEvery = 5 * time.Second } heartbeat := time.NewTicker(heartbeatEvery) defer heartbeat.Stop() poll := time.NewTicker(pollEvery) defer poll.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-heartbeat.C: report := HealthReport{Status: "healthy", Reason: "typed companion dispatcher ready"} if runtime.Health != nil { report = runtime.Health() } if _, err := runtime.Client.Heartbeat(ctx, report); err != nil { return err } case <-poll.C: if err := runtime.Dispatcher.DispatchOnce(ctx); err != nil { select { case <-ctx.Done(): return ctx.Err() case <-time.After(runtime.backoff()): } } } } } func (runtime Runtime) backoff() time.Duration { if runtime.Backoff > 0 { return runtime.Backoff } return 2 * time.Second }