package validator import ( "fmt" "net/url" "strings" "browser.local/platform/domain" ) const maxDistributionMessageLength = 256 func ValidateRuntimeBinding(binding domain.RuntimeBinding) error { var violations []string violations = appendRequired(violations, "id", binding.ID) violations = appendRequired(violations, "serverInstanceId", binding.ServerInstanceID) violations = appendRequired(violations, "pluginId", binding.PluginID) violations = appendRequired(violations, "pluginVersion", binding.PluginVersion) violations = appendRequired(violations, "profileKey", binding.ProfileKey) violations = appendRequired(violations, "mode", binding.Mode) if !validRuntimeBindingStatus(binding.Status) { violations = append(violations, "status is invalid") } for key, value := range binding.Bindings { if !validDistributionLogicalKey(key) { violations = append(violations, "bindings key is invalid") } trimmed := strings.TrimSpace(value) lowerKey := strings.ToLower(key) sensitiveKey := strings.Contains(lowerKey, "password") || strings.Contains(lowerKey, "credential") || strings.Contains(lowerKey, "secret") || strings.Contains(lowerKey, "token") || strings.Contains(lowerKey, "dsn") if trimmed != value || strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\`) || containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "://") && !strings.HasPrefix(value, "secret://") || sensitiveKey && value != "" && !strings.HasPrefix(value, "secret://") { violations = append(violations, "bindings."+key+" must use safe logical or secret refs") } } for i, key := range binding.MissingKeys { if !validDistributionLogicalKey(key) { violations = append(violations, fmt.Sprintf("missingKeys[%d] is invalid", i)) } } if binding.CreatedAt.IsZero() { violations = append(violations, "createdAt is required") } if binding.UpdatedAt.IsZero() { violations = append(violations, "updatedAt is required") } return finish(violations) } func ValidateEncryptedComponentKey(key domain.EncryptedComponentKey) error { var violations []string violations = appendRequired(violations, "id", key.ID) violations = appendRequired(violations, "serverInstanceId", key.ServerInstanceID) violations = appendRequired(violations, "encryptedKey", key.EncryptedKey) violations = appendRequired(violations, "keyHash", key.KeyHash) violations = appendRequired(violations, "fingerprint", key.Fingerprint) violations = appendRequired(violations, "secretRef", key.SecretRef) 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") } if !strings.HasPrefix(key.EncryptedKey, "enc:v1:") { violations = append(violations, "encryptedKey must be encrypted") } if key.KeyHash != "" && !validSHA256Checksum(key.KeyHash) { violations = append(violations, "keyHash must be sha256:") } if !strings.HasPrefix(key.SecretRef, "secret://runtime-keys/") { violations = append(violations, "secretRef must be a runtime key secret ref") } if key.Generation <= 0 { violations = append(violations, "generation must be positive") } if !validComponentKeyStatus(key.Status) { violations = append(violations, "status is invalid") } if key.CreatedAt.IsZero() { violations = append(violations, "createdAt is required") } if key.UpdatedAt.IsZero() { violations = append(violations, "updatedAt is required") } return finish(violations) } func ValidateRunDistribution(distribution domain.RunDistribution) 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, "runEndpointId", distribution.RunEndpointID) violations = appendRequired(violations, "targetOs", distribution.TargetOS) violations = appendRequired(violations, "targetArch", distribution.TargetArch) violations = appendRequired(violations, "packageFormat", distribution.PackageFormat) 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 distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) { violations = append(violations, "checksum must be sha256:") } 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") } if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" && distribution.PackageFormat != "raw-executable" { violations = append(violations, "packageFormat is invalid") } 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 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:") } 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) violations = appendRequired(violations, "serverInstanceId", status.ServerInstanceID) violations = appendRequired(violations, "pluginId", status.PluginID) violations = appendRequired(violations, "probeKey", status.ProbeKey) if !validDistributionLogicalKey(status.ProbeKey) { violations = append(violations, "probeKey is invalid") } if status.TargetOS != "" || status.TargetArch != "" { violations = appendDistributionTargetViolations(violations, status.TargetOS, status.TargetArch) } if !validDependencyState(status.State) { violations = append(violations, "state is invalid") } if status.InstallPlanKey != "" && !validDistributionLogicalKey(status.InstallPlanKey) { violations = append(violations, "installPlanKey is invalid") } if status.PlanDigest != "" && !validSHA256Checksum(status.PlanDigest) { violations = append(violations, "planDigest must be sha256:") } if len(status.JobID) > 180 || containsUnsafeRuntimeSecret(status.JobID) || looksLikeRawHostPath(status.JobID) { violations = append(violations, "jobId is unsafe or too long") } if status.CompletedSteps < 0 || status.CompletedSteps > 64 { violations = append(violations, "completedSteps is out of bounds") } if len(status.Evidence) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(status.Evidence) || looksLikeRawHostPath(status.Evidence) { violations = append(violations, "evidence is unsafe or too long") } if len(status.Message) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(status.Message) || looksLikeRawHostPath(status.Message) { violations = append(violations, "message is unsafe or too long") } if status.CheckedAt.IsZero() { violations = append(violations, "checkedAt is required") } if status.UpdatedAt.IsZero() { violations = append(violations, "updatedAt is required") } 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:") } 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) violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID) violations = appendRequired(violations, "runEndpointId", job.RunEndpointID) violations = appendRequired(violations, "artifactId", job.ArtifactID) violations = appendRequired(violations, "checksum", job.Checksum) violations = appendRequired(violations, "targetOs", job.TargetOS) violations = appendRequired(violations, "targetArch", job.TargetArch) violations = appendRequired(violations, "targetRelease", job.TargetRelease) violations = appendRequired(violations, "idempotencyKey", job.IdempotencyKey) if job.Checksum != "" && !validSHA256Checksum(job.Checksum) { violations = append(violations, "checksum must be sha256:") } if !validDistributionJobStatus(job.Status) { violations = append(violations, "status is invalid") } violations = appendDistributionTargetViolations(violations, job.TargetOS, job.TargetArch) if !validRunUpdatePhase(job.Phase) { violations = append(violations, "phase is invalid") } if len(job.Message) > maxDistributionMessageLength || containsUnsafeRuntimeSecret(job.Message) || looksLikeRawHostPath(job.Message) { violations = append(violations, "message is unsafe or too long") } if containsUnsafeRuntimeSecret(job.IdempotencyKey) || looksLikeRawHostPath(job.IdempotencyKey) { violations = append(violations, "idempotencyKey is unsafe") } if job.CreatedAt.IsZero() { violations = append(violations, "createdAt is required") } if job.UpdatedAt.IsZero() { violations = append(violations, "updatedAt is required") } return finish(violations) } func validRunUpdatePhase(phase domain.RunUpdatePhase) bool { switch phase { case domain.RunUpdatePhaseQueued, domain.RunUpdatePhaseDownloading, domain.RunUpdatePhaseStaged, domain.RunUpdatePhaseRestartRequested, domain.RunUpdatePhaseActivating, domain.RunUpdatePhaseSucceeded, domain.RunUpdatePhaseRolledBack, domain.RunUpdatePhaseFailed: return true default: return false } } func ValidateRunDistributionGenerateRequest(request domain.RunDistributionGenerateRequest) error { var violations []string violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "targetOs", request.TargetOS) violations = appendRequired(violations, "targetArch", request.TargetArch) violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey) violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch) if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) { violations = append(violations, "idempotencyKey is unsafe") } 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) 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.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) { violations = append(violations, "componentKey is invalid") } return finish(violations) } func ValidateComponentAuthenticationRequest(request domain.ComponentAuthenticationRequest) error { var violations []string violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "key", request.Key) 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.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) { violations = append(violations, "componentKey is invalid") } if request.Generation <= 0 { violations = append(violations, "generation must be positive") } if len(request.Key) > 256 || looksLikeRawHostPath(request.Key) || strings.Contains(strings.ToLower(request.Key), "://") { violations = append(violations, "key is unsafe") } return finish(violations) } func appendDistributionTargetViolations(violations []string, targetOS string, targetArch string) []string { if !validDistributionTargetOS(targetOS) { violations = append(violations, "targetOs is invalid") } if !validDistributionTargetArch(targetArch) { violations = append(violations, "targetArch is invalid") } return violations } func appendDistributionStatusViolations(violations []string, status domain.DistributionStatus) []string { if !validDistributionStatus(status) { return append(violations, "status is invalid") } 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, domain.DistributionComponentClientManager: return true default: return false } } func validComponentKeyStatus(status domain.ComponentKeyStatus) bool { switch status { case domain.ComponentKeyStatusActive, domain.ComponentKeyStatusRevoked: return true default: return false } } func validDistributionStatus(status domain.DistributionStatus) bool { switch status { case domain.DistributionStatusAvailable, domain.DistributionStatusRevoked, domain.DistributionStatusBuilding, domain.DistributionStatusFailed: return true default: return false } } func validRuntimeBindingStatus(status domain.RuntimeBindingStatus) bool { switch status { case domain.RuntimeBindingStatusComplete, domain.RuntimeBindingStatusIncomplete: return true default: return false } } func validDependencyState(state domain.DependencyState) bool { switch state { case domain.DependencyStateUnknown, domain.DependencyStatePresent, domain.DependencyStateMissing, domain.DependencyStateInstalling, domain.DependencyStateFailed: return true default: return false } } func validDistributionJobStatus(status domain.DistributionJobStatus) bool { switch status { case domain.DistributionJobStatusQueued, domain.DistributionJobStatusRunning, domain.DistributionJobStatusSucceeded, domain.DistributionJobStatusFailed, domain.DistributionJobStatusDenied: return true default: return false } } func validDistributionTargetOS(targetOS string) bool { switch targetOS { case "linux", "windows", "darwin": return true default: return false } } func validDistributionTargetArch(targetArch string) bool { switch targetArch { case "amd64", "arm64": return true default: return false } } func validDistributionLogicalKey(value string) bool { trimmed := strings.TrimSpace(value) if trimmed == "" || trimmed != value || len([]rune(value)) > 96 { return false } for _, char := range value { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' { continue } return false } return !strings.Contains(value, "..") && !strings.Contains(value, "://") && !looksLikeRawHostPath(value) && !containsUnsafeRuntimeSecret(value) }