Remove legacy client-manager platform path
This commit is contained in:
@@ -1,293 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
var clientManagerIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$`)
|
||||
|
||||
func ValidateClientManagerInstallation(value domain.ClientManagerInstallation) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", value.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", value.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", value.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", value.ProfileKey)
|
||||
violations = appendRequired(violations, "runEndpointId", value.RunEndpointID)
|
||||
if !validDistributionLogicalKey(value.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if value.TargetOS != "" || value.TargetArch != "" {
|
||||
violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch)
|
||||
}
|
||||
if !validClientManagerLifecycleStatus(value.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validClientManagerHealth(value.Health) {
|
||||
violations = append(violations, "health is invalid")
|
||||
}
|
||||
if value.KeyGeneration < 0 || value.DeploymentGeneration < 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must not be negative")
|
||||
}
|
||||
if value.Checksum != "" && !validSHA256Checksum(value.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
for field, content := range map[string]string{"phase": value.Phase, "healthReason": value.HealthReason} {
|
||||
if len(content) > 240 || unsafeLifecycleText(content) {
|
||||
violations = append(violations, field+" must be bounded and redacted")
|
||||
}
|
||||
}
|
||||
if value.CreatedAt.IsZero() || value.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt and updatedAt are required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerLifecycleTransition(from, to domain.ClientManagerLifecycleStatus) error {
|
||||
if from == to {
|
||||
return nil
|
||||
}
|
||||
allowed := map[domain.ClientManagerLifecycleStatus][]domain.ClientManagerLifecycleStatus{
|
||||
domain.ClientManagerLifecycleRequested: {domain.ClientManagerLifecycleBuilding, domain.ClientManagerLifecycleAvailable, domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleBuilding: {domain.ClientManagerLifecycleAvailable, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleAvailable: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleDeploying: {domain.ClientManagerLifecycleInstalled, domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleFailed, domain.ClientManagerLifecycleUninstalled},
|
||||
domain.ClientManagerLifecycleInstalled: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleRegistering: {domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleOnline: {domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleDegraded: {domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleOffline: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleUpdating: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleRollingBack: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleStopping: {domain.ClientManagerLifecycleInstalled, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleUninstalled: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleFailed: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled},
|
||||
}
|
||||
for _, candidate := range allowed[from] {
|
||||
if candidate == to {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return finish([]string{"client-manager lifecycle transition is invalid"})
|
||||
}
|
||||
|
||||
func ValidateClientManagerSession(value domain.ClientManagerSession) error {
|
||||
var violations []string
|
||||
for field, content := range map[string]string{"id": value.ID, "installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "profileKey": value.ProfileKey, "runEndpointId": value.RunEndpointID, "artifactId": value.ArtifactID, "tokenHash": value.TokenHash} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must be positive")
|
||||
}
|
||||
if len(value.TokenHash) != 64 || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(value.TokenHash) {
|
||||
violations = append(violations, "tokenHash must be a SHA-256 digest")
|
||||
}
|
||||
if !oneOf(string(value.Status), string(domain.ClientManagerSessionActive), string(domain.ClientManagerSessionRevoked), string(domain.ClientManagerSessionExpired)) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() || value.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "session timestamps are required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerNonce(value domain.ClientManagerRegistrationNonce) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", value.ID)
|
||||
violations = appendRequired(violations, "installationId", value.InstallationID)
|
||||
if len(value.ID) != 64 || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(value.ID) {
|
||||
violations = append(violations, "id must be a nonce SHA-256 digest")
|
||||
}
|
||||
if value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() || !value.ExpiresAt.After(value.CreatedAt) {
|
||||
violations = append(violations, "nonce expiry must follow creation")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerDeployRequest(value domain.ClientManagerDeployRequest) error {
|
||||
return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, value.DistributionID, value.IdempotencyKey, value.ExpectedDeploymentGeneration, true)
|
||||
}
|
||||
|
||||
func ValidateClientManagerControlRequest(value domain.ClientManagerControlRequest) error {
|
||||
if !oneOf(string(value.Operation), "start", "stop", "restart", "status", "rollback") {
|
||||
return finish([]string{"operation is invalid"})
|
||||
}
|
||||
return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false)
|
||||
}
|
||||
|
||||
func ValidateClientManagerUpdateRequest(value domain.ClientManagerUpdateRequest) error {
|
||||
var violations []string
|
||||
if !value.Approved {
|
||||
violations = append(violations, "approved must be true")
|
||||
}
|
||||
if err := validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, value.DistributionID, value.IdempotencyKey, value.ExpectedDeploymentGeneration, true); err != nil {
|
||||
violations = append(violations, err.Error())
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerUninstallRequest(value domain.ClientManagerUninstallRequest) error {
|
||||
var violations []string
|
||||
if !value.Confirmed {
|
||||
violations = append(violations, "confirmed must be true")
|
||||
}
|
||||
if err := validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false); err != nil {
|
||||
violations = append(violations, err.Error())
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerRetryRequest(value domain.ClientManagerRetryRequest) error {
|
||||
return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false)
|
||||
}
|
||||
|
||||
func ValidateClientManagerLifecycleInputRequest(value domain.ClientManagerLifecycleInputRequest) error {
|
||||
var violations []string
|
||||
for field, content := range map[string]string{"runEndpointId": value.RunEndpointID, "sessionToken": value.SessionToken, "jobId": value.JobID, "leaseToken": value.LeaseToken} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
if value.Attempt <= 0 {
|
||||
violations = append(violations, "attempt must be positive")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerCompanionConfigInput(value domain.ClientManagerCompanionConfigInput) error {
|
||||
var violations []string
|
||||
if value.SchemaVersion != domain.ClientManagerCompanionConfigSchemaVersion {
|
||||
violations = append(violations, "schemaVersion is invalid")
|
||||
}
|
||||
for field, content := range map[string]string{
|
||||
"configTemplateKey": value.ConfigTemplateKey, "configTemplateRef": value.ConfigTemplateRef, "configOutputRef": value.ConfigOutputRef,
|
||||
"configSchemaRef": value.ConfigSchemaRef, "installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID,
|
||||
"pluginId": value.PluginID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID, "version": value.Version,
|
||||
"sourceRevision": value.SourceRevision, "targetOs": value.TargetOS, "targetArch": value.TargetArch, "proofMaterialEnv": value.ProofMaterialEnv,
|
||||
} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
if !clientManagerIdentifierPattern.MatchString(value.ConfigTemplateKey) {
|
||||
violations = append(violations, "configTemplateKey is invalid")
|
||||
}
|
||||
violations = append(violations, validateSafeRelativeRuntimePath("configTemplateRef", value.ConfigTemplateRef)...)
|
||||
if value.ConfigOutputRef != "config.yaml" {
|
||||
violations = append(violations, "configOutputRef must be config.yaml")
|
||||
}
|
||||
if !safeRelativeJSONRef(value.ConfigSchemaRef) {
|
||||
violations = append(violations, "configSchemaRef must be a safe relative JSON reference")
|
||||
}
|
||||
if value.ConfigFormat != "yaml" || value.PlatformBaseURLSource != "run-control" || value.RegistrationProof != "hmac-sha256" || value.ProofMaterialSource != "component-package" || value.SessionMode != "component-session" || value.TLSPolicy != "verify-system-roots" {
|
||||
violations = append(violations, "companion bootstrap security policy is invalid")
|
||||
}
|
||||
if !validCompanionProofEnvironment(value.ProofMaterialEnv) {
|
||||
violations = append(violations, "proofMaterialEnv is invalid")
|
||||
}
|
||||
for field, identifier := range map[string]string{"installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "pluginId": value.PluginID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID} {
|
||||
if !clientManagerIdentifierPattern.MatchString(identifier) {
|
||||
violations = append(violations, field+" is invalid")
|
||||
}
|
||||
}
|
||||
violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch)
|
||||
if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must be positive")
|
||||
}
|
||||
if len(value.Capabilities) == 0 || len(value.Capabilities) > 32 {
|
||||
violations = append(violations, "capabilities must be bounded")
|
||||
}
|
||||
capabilities := make(map[string]struct{}, len(value.Capabilities))
|
||||
for _, capability := range value.Capabilities {
|
||||
if !clientManagerIdentifierPattern.MatchString(capability) {
|
||||
violations = append(violations, "capability is invalid")
|
||||
}
|
||||
if _, exists := capabilities[capability]; exists {
|
||||
violations = append(violations, "capabilities must be unique")
|
||||
}
|
||||
capabilities[capability] = struct{}{}
|
||||
}
|
||||
for _, required := range []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"} {
|
||||
if _, exists := capabilities[required]; !exists {
|
||||
violations = append(violations, "capabilities must include "+required)
|
||||
}
|
||||
}
|
||||
if value.HeartbeatIntervalSeconds < 5 || value.HeartbeatIntervalSeconds > 300 || value.CommandPollIntervalSeconds < 1 || value.CommandPollIntervalSeconds > 60 || value.RequestTimeoutSeconds < 1 || value.RequestTimeoutSeconds > 60 {
|
||||
violations = append(violations, "companion timing policy is invalid")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerRegisterRequest(value domain.ClientManagerRegisterRequest) error {
|
||||
var violations []string
|
||||
for field, content := range map[string]string{"installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID, "version": value.Version, "sourceRevision": value.SourceRevision, "targetOs": value.TargetOS, "targetArch": value.TargetArch, "nonce": value.Nonce, "signature": value.Signature} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch)
|
||||
if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must be positive")
|
||||
}
|
||||
if value.Timestamp.IsZero() {
|
||||
violations = append(violations, "timestamp is required")
|
||||
}
|
||||
if !regexp.MustCompile(`^[A-Za-z0-9_-]{16,128}$`).MatchString(value.Nonce) {
|
||||
violations = append(violations, "nonce is invalid")
|
||||
}
|
||||
if !regexp.MustCompile(`^sha256:[a-f0-9]{64}$`).MatchString(value.Signature) {
|
||||
violations = append(violations, "signature is invalid")
|
||||
}
|
||||
if len(value.Capabilities) == 0 {
|
||||
violations = append(violations, "capabilities must not be empty")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerHeartbeat(value domain.ClientManagerHeartbeat) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "installationId", value.InstallationID)
|
||||
violations = appendRequired(violations, "sessionToken", value.SessionToken)
|
||||
if value.Sequence == 0 {
|
||||
violations = append(violations, "sequence must be positive")
|
||||
}
|
||||
if !validClientManagerHealth(value.Health) || value.Health == domain.ClientManagerHealthUnknown {
|
||||
violations = append(violations, "health is invalid")
|
||||
}
|
||||
if len(value.HealthReason) > 240 || unsafeLifecycleText(value.HealthReason) {
|
||||
violations = append(violations, "healthReason must be bounded and redacted")
|
||||
}
|
||||
if value.SentAt.IsZero() {
|
||||
violations = append(violations, "sentAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validateClientManagerOperationRequest(serverID, profileKey, distributionID, idempotencyKey string, generation int, distributionRequired bool) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", serverID)
|
||||
violations = appendRequired(violations, "profileKey", profileKey)
|
||||
violations = appendRequired(violations, "idempotencyKey", idempotencyKey)
|
||||
if distributionRequired {
|
||||
violations = appendRequired(violations, "distributionId", distributionID)
|
||||
}
|
||||
if !validDistributionLogicalKey(profileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if !clientManagerIdentifierPattern.MatchString(idempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is invalid")
|
||||
}
|
||||
if generation < 0 {
|
||||
violations = append(violations, "expectedDeploymentGeneration must not be negative")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validClientManagerLifecycleStatus(value domain.ClientManagerLifecycleStatus) bool {
|
||||
return oneOf(string(value), "requested", "building", "available", "deploying", "installed", "registering", "online", "degraded", "offline", "updating", "rolling_back", "stopping", "uninstalled", "failed")
|
||||
}
|
||||
|
||||
func validClientManagerHealth(value domain.ClientManagerHealthStatus) bool {
|
||||
return oneOf(string(value), "unknown", "healthy", "degraded", "unhealthy", "offline")
|
||||
}
|
||||
|
||||
func unsafeLifecycleText(value string) bool {
|
||||
lowered := strings.ToLower(strings.TrimSpace(value))
|
||||
return strings.Contains(lowered, "secret://") || strings.Contains(lowered, "bearer ") || strings.Contains(lowered, "password=") || strings.Contains(lowered, "token=") || strings.Contains(lowered, "unix://") || strings.Contains(lowered, "tcp://") || looksLikeRawHostPath(value)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestValidateRuntimeProfilesRejectsLegacyClientManagers(t *testing.T) {
|
||||
profile := domain.RuntimeClientManagerProfile{Key: "scum-client-manager", DisplayName: "SCUM Client Manager"}
|
||||
err := ValidateGamePluginRuntimeProfiles(domain.GamePluginRuntimeProfiles{ClientManagers: []domain.RuntimeClientManagerProfile{profile}})
|
||||
if err == nil || !strings.Contains(err.Error(), "runtimeProfiles.clientManagers is no longer supported") {
|
||||
t.Fatalf("expected legacy client-manager profile rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientManagerPersistenceContractsFailClosed(t *testing.T) {
|
||||
stamp := time.Date(2026, 7, 18, 4, 0, 0, 0, time.UTC)
|
||||
installation := domain.ClientManagerInstallation{ID: "cm-install-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client-manager", RunEndpointID: "run-1", TargetOS: "linux", TargetArch: "amd64", Status: domain.ClientManagerLifecycleOnline, Phase: "healthy", KeyGeneration: 1, DeploymentGeneration: 1, Health: domain.ClientManagerHealthHealthy, HealthReason: "ready", CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := ValidateClientManagerInstallation(installation); err != nil {
|
||||
t.Fatalf("validate historical installation projection: %v", err)
|
||||
}
|
||||
installation.HealthReason = "Bearer stolen-session"
|
||||
if err := ValidateClientManagerInstallation(installation); err == nil {
|
||||
t.Fatal("expected session-bearing health reason rejection")
|
||||
}
|
||||
session := domain.ClientManagerSession{ID: "cm-session-1", InstallationID: "cm-install-1", ServerInstanceID: "server-1", ProfileKey: "scum-client-manager", RunEndpointID: "run-1", ArtifactID: "artifact-1", KeyGeneration: 1, DeploymentGeneration: 1, TokenHash: strings.Repeat("a", 64), Capabilities: []string{"component.heartbeat"}, Status: domain.ClientManagerSessionActive, ExpiresAt: stamp.Add(time.Minute), CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := ValidateClientManagerSession(session); err != nil {
|
||||
t.Fatalf("validate historical hashed session: %v", err)
|
||||
}
|
||||
if err := ValidateClientManagerHeartbeat(domain.ClientManagerHeartbeat{InstallationID: "cm-install-1", SessionToken: "component-session", Sequence: 1, Health: domain.ClientManagerHealthHealthy, HealthReason: "password=leak", Capabilities: []string{"component.heartbeat"}, SentAt: stamp}); err == nil {
|
||||
t.Fatal("expected unsafe heartbeat reason rejection")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -57,9 +56,6 @@ func ValidateEncryptedComponentKey(key domain.EncryptedComponentKey) error {
|
||||
if !validDistributionComponentKind(key.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if key.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(key.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager keys")
|
||||
}
|
||||
if key.ComponentKey != "" && !validDistributionLogicalKey(key.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
}
|
||||
@@ -125,44 +121,6 @@ func ValidateRunDistribution(distribution domain.RunDistribution) error {
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerDistribution(distribution domain.ClientManagerDistribution) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", distribution.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", distribution.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", distribution.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", distribution.ProfileKey)
|
||||
violations = appendRequired(violations, "repositoryUrl", distribution.RepositoryURL)
|
||||
violations = appendRequired(violations, "sourceRevision", distribution.SourceRevision)
|
||||
violations = appendRequired(violations, "buildJobId", distribution.BuildJobID)
|
||||
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
|
||||
violations = appendRequired(violations, "secretRef", distribution.SecretRef)
|
||||
violations = appendDistributionTargetViolations(violations, distribution.TargetOS, distribution.TargetArch)
|
||||
violations = appendDistributionStatusViolations(violations, distribution.Status)
|
||||
if !validDistributionLogicalKey(distribution.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if distribution.Status == domain.DistributionStatusAvailable && distribution.Checksum == "" {
|
||||
violations = append(violations, "checksum is required when distribution is available")
|
||||
}
|
||||
if distribution.KeyGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration must be positive")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", distribution.RepositoryURL)...)
|
||||
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
|
||||
violations = append(violations, "secretRef must be redacted runtime key ref")
|
||||
}
|
||||
if distribution.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if distribution.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateDependencyStatus(status domain.DependencyStatus) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", status.ID)
|
||||
@@ -205,40 +163,6 @@ func ValidateDependencyStatus(status domain.DependencyStatus) error {
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerBuildJob(job domain.ClientManagerBuildJob) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", job.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", job.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", job.ProfileKey)
|
||||
violations = appendRequired(violations, "repositoryUrl", job.RepositoryURL)
|
||||
violations = appendRequired(violations, "sourceRevision", job.SourceRevision)
|
||||
violations = appendDistributionTargetViolations(violations, job.TargetOS, job.TargetArch)
|
||||
if !validDistributionLogicalKey(job.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if job.Checksum != "" && !validSHA256Checksum(job.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if job.LogsRef != "" && !validScopedInputRef(job.LogsRef) {
|
||||
violations = append(violations, "logsRef is not allowed")
|
||||
}
|
||||
if job.KeyGeneration < 0 {
|
||||
violations = append(violations, "keyGeneration must not be negative")
|
||||
}
|
||||
if !validDistributionJobStatus(job.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", job.RepositoryURL)...)
|
||||
if job.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if job.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateRunUpdateJob(job domain.RunUpdateJob) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", job.ID)
|
||||
@@ -297,28 +221,6 @@ func ValidateRunDistributionGenerateRequest(request domain.RunDistributionGenera
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerBuildRequest(request domain.ClientManagerBuildRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
violations = appendRequired(violations, "profileKey", request.ProfileKey)
|
||||
violations = appendRequired(violations, "targetOs", request.TargetOS)
|
||||
violations = appendRequired(violations, "targetArch", request.TargetArch)
|
||||
violations = appendRequired(violations, "repositoryUrl", request.RepositoryURL)
|
||||
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
|
||||
violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch)
|
||||
if !validDistributionLogicalKey(request.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if request.SourceRevision != "" && !validDistributionLogicalKey(request.SourceRevision) {
|
||||
violations = append(violations, "sourceRevision is invalid")
|
||||
}
|
||||
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is unsafe")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", request.RepositoryURL)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateComponentKeyResetRequest(request domain.ComponentKeyResetRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
@@ -341,8 +243,8 @@ func ValidateComponentAuthenticationRequest(request domain.ComponentAuthenticati
|
||||
if !validDistributionComponentKind(request.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if request.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(request.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager")
|
||||
if request.ComponentKind != domain.DistributionComponentRun {
|
||||
violations = append(violations, "componentKind must be run")
|
||||
}
|
||||
if request.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
@@ -373,20 +275,6 @@ func appendDistributionStatusViolations(violations []string, status domain.Distr
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateRepositoryURL(field string, value string) []string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := url.ParseRequestURI(strings.TrimSpace(value))
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || !strings.HasSuffix(parsed.Path, ".git") {
|
||||
return []string{field + " must be an HTTPS git repository URL"}
|
||||
}
|
||||
for _, reason := range unsafePluginStringReasons(value) {
|
||||
return []string{field + ": " + reason}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validDistributionComponentKind(kind domain.DistributionComponentKind) bool {
|
||||
switch kind {
|
||||
case domain.DistributionComponentRun:
|
||||
|
||||
@@ -109,7 +109,7 @@ func validateRemoteAdapterInputs(field string, inputs map[string]string) []strin
|
||||
}
|
||||
var violations []string
|
||||
for key, value := range inputs {
|
||||
if !clientManagerIdentifierPattern.MatchString(key) || unsafeRemoteAdapterInputKey(key) {
|
||||
if !runtimeIdentifierPattern.MatchString(key) || unsafeRemoteAdapterInputKey(key) {
|
||||
violations = append(violations, field+" key is invalid or unsafe")
|
||||
}
|
||||
limit := 2048
|
||||
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
gameClientBridgeCollectionPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,119}$`)
|
||||
gameClientBridgeFieldPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,79}$`)
|
||||
gameClientBridgeCaptureNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,79}$`)
|
||||
runtimeIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$`)
|
||||
)
|
||||
|
||||
type ValidationError struct {
|
||||
@@ -487,7 +488,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
commandTypes := map[string]struct{}{}
|
||||
for index, command := range bridge.Commands {
|
||||
prefix := fmt.Sprintf("%s.commands[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(command.Type) || unsafeGameClientBridgeCommandType(command.Type) {
|
||||
if !runtimeIdentifierPattern.MatchString(command.Type) || unsafeGameClientBridgeCommandType(command.Type) {
|
||||
violations = append(violations, prefix+".type is invalid or unsafe")
|
||||
}
|
||||
if _, exists := commandTypes[command.Type]; exists {
|
||||
@@ -514,7 +515,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
for index, snapshot := range bridge.Snapshots {
|
||||
prefix := fmt.Sprintf("%s.snapshots[%d]", field, index)
|
||||
key := snapshot.Type + "\x00" + snapshot.SchemaVersion
|
||||
if !clientManagerIdentifierPattern.MatchString(snapshot.Type) || !clientManagerIdentifierPattern.MatchString(snapshot.SchemaVersion) {
|
||||
if !runtimeIdentifierPattern.MatchString(snapshot.Type) || !runtimeIdentifierPattern.MatchString(snapshot.SchemaVersion) {
|
||||
violations = append(violations, prefix+" type or schemaVersion is invalid")
|
||||
}
|
||||
if _, exists := snapshotTypes[key]; exists {
|
||||
@@ -531,7 +532,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
queryTemplates := map[string]domain.GameClientBridgeQueryTemplateDeclaration{}
|
||||
for index, template := range bridge.QueryTemplates {
|
||||
prefix := fmt.Sprintf("%s.queryTemplates[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(template.Key) {
|
||||
if !runtimeIdentifierPattern.MatchString(template.Key) {
|
||||
violations = append(violations, prefix+".key is invalid")
|
||||
}
|
||||
if _, exists := queryTemplates[template.Key]; exists {
|
||||
@@ -583,7 +584,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
lifecycleProjectionKeys := map[string]struct{}{}
|
||||
for index, projection := range bridge.LifecycleProjections {
|
||||
prefix := fmt.Sprintf("%s.lifecycleProjections[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(projection.Key) {
|
||||
if !runtimeIdentifierPattern.MatchString(projection.Key) {
|
||||
violations = append(violations, prefix+".key is invalid")
|
||||
}
|
||||
if _, exists := lifecycleProjectionKeys[projection.Key]; exists {
|
||||
@@ -620,7 +621,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
features := map[string]domain.GameClientBridgeFeatureDeclaration{}
|
||||
for index, feature := range bridge.Features {
|
||||
prefix := fmt.Sprintf("%s.features[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
|
||||
if !runtimeIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
|
||||
violations = append(violations, prefix+".key is invalid or unsafe")
|
||||
}
|
||||
if _, exists := features[feature.Key]; exists {
|
||||
@@ -637,12 +638,12 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
violations = append(violations, prefix+" must require a handler or event producer")
|
||||
}
|
||||
for _, handler := range feature.RequiredHandlers {
|
||||
if !clientManagerIdentifierPattern.MatchString(handler) {
|
||||
if !runtimeIdentifierPattern.MatchString(handler) {
|
||||
violations = append(violations, prefix+".requiredHandlers contains an invalid handler")
|
||||
}
|
||||
}
|
||||
for _, producer := range feature.RequiredEventProducers {
|
||||
if !clientManagerIdentifierPattern.MatchString(producer) {
|
||||
if !runtimeIdentifierPattern.MatchString(producer) {
|
||||
violations = append(violations, prefix+".requiredEventProducers contains an invalid producer")
|
||||
}
|
||||
}
|
||||
@@ -1916,7 +1917,7 @@ func validatePluginPages(pages []domain.GamePluginPage) []string {
|
||||
violations = append(violations, duplicateViolations(prefix+".permissions", page.Permissions)...)
|
||||
violations = append(violations, validateBridgeActions(prefix+".bridgeActions", page.BridgeActions)...)
|
||||
for _, key := range page.FeatureKeys {
|
||||
if !clientManagerIdentifierPattern.MatchString(key) {
|
||||
if !runtimeIdentifierPattern.MatchString(key) {
|
||||
violations = append(violations, prefix+".featureKeys contains an invalid feature key")
|
||||
}
|
||||
}
|
||||
@@ -2349,7 +2350,7 @@ func validPluginMarketplaceStateAction(action domain.PluginMarketplaceStateActio
|
||||
|
||||
func validPluginRunCapability(capability string) bool {
|
||||
switch capability {
|
||||
case "process.install", "process.start", "process.stop", "process.restart", "process.status",
|
||||
case "process.install", "process.start", "process.stop", "process.restart", "process.status",
|
||||
"config.write",
|
||||
"files.list", "files.read", "files.write", "files.patch",
|
||||
"file.list", "file.read", "file.write", "file.patch",
|
||||
@@ -2362,7 +2363,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
||||
domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd,
|
||||
"artifacts.read", "artifacts.write", "artifact.read", "artifact.write",
|
||||
"ai.invoke":
|
||||
return true
|
||||
|
||||
@@ -47,7 +47,7 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
prefix := fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", profile.Key)...)
|
||||
violations = append(violations, recordRuntimeProfileKey(lifecycleKeys, prefix+".key", profile.Key)...)
|
||||
if !oneOf(profile.Mode, "local-process", "hosted-ftp-rcon", "ftp-only", "custom-client") {
|
||||
if !oneOf(profile.Mode, "local-process", "hosted-ftp-rcon", "ftp-only") {
|
||||
violations = append(violations, prefix+".mode is invalid")
|
||||
}
|
||||
if len(profile.Capabilities) == 0 {
|
||||
@@ -64,9 +64,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
violations = append(violations, validateProfileKey(fmt.Sprintf("%s.transportKeys[%d]", prefix, j), key)...)
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".transportKeys", profile.TransportKeys)...)
|
||||
if profile.ClientManagerRef != "" {
|
||||
violations = append(violations, prefix+".clientManagerRef is no longer supported")
|
||||
}
|
||||
for j, key := range profile.DLLExtensionRefs {
|
||||
violations = append(violations, validateProfileKey(fmt.Sprintf("%s.dllExtensionRefs[%d]", prefix, j), key)...)
|
||||
}
|
||||
@@ -298,9 +295,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
violations = append(violations, prefix+".transportKey must reference a declared SQLite query transport")
|
||||
}
|
||||
}
|
||||
if len(profiles.ClientManagers) > 0 {
|
||||
violations = append(violations, "runtimeProfiles.clientManagers is no longer supported")
|
||||
}
|
||||
for i, extension := range profiles.DLLExtensions {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.dllExtensions[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", extension.Key)...)
|
||||
@@ -495,8 +489,8 @@ func validateRuntimeProfileCapabilityDeclarations(profiles domain.GamePluginRunt
|
||||
for i, transport := range profiles.TransportProfiles {
|
||||
check(fmt.Sprintf("runtimeProfiles.transportProfiles[%d].capabilities", i), transport.Capabilities)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateLifecycleActionsOptional(actions domain.PluginLifecycleActions) []string {
|
||||
var violations []string
|
||||
|
||||
Reference in New Issue
Block a user