package companion import ( "context" "fmt" "strings" "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) } type HandlerAvailability struct { 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 } func NewHandlerRegistry(availability HandlerAvailability, adapter SafeAdapter) *HandlerRegistry { registry := &HandlerRegistry{availability: availability, handlers: map[string]CommandHandler{}} 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) } return registry } func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCommand) (CommandResult, error) { if err := validateDeclaredCommand(command); err != nil { return unsupportedResult("validation-failed"), nil } if !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 } return CommandResult{Status: "succeeded", Summary: "typed adapter completed", Payload: redactTypedPayload(payload)}, nil } func validateDeclaredCommand(command ClaimedCommand) error { if command.ID == "" || command.ProfileKey != ProfileKey || command.FencingToken == 0 || command.Payload == nil || command.ExpiresAt.IsZero() || !time.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 nil } 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 Dispatcher struct { Client *Client; Registry *HandlerRegistry; PollLimit int; Backoff time.Duration } 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 = 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) 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): } } }