// Package domain contains pure domain models with no external dependencies. package domain import ( "fmt" "maps" "net/url" "strconv" "strings" "time" ) // BuildSpec defines what a build task should accomplish. // It captures the user's intent and parameters for code generation. type BuildSpec struct { // Template is the project template to use (e.g., "nextjs-landing"). Template string `json:"template,omitempty"` // Prompt is the user's instruction for the build. Prompt string `json:"prompt"` // Variables contains template-specific substitution values. Variables map[string]string `json:"variables,omitempty"` // AutoCommit controls whether changes are automatically committed. AutoCommit bool `json:"auto_commit"` // AutoPush controls whether commits are automatically pushed. AutoPush bool `json:"auto_push"` // CallbackURL is the webhook URL for completion notification. CallbackURL string `json:"callback_url,omitempty"` // GitCloneURL is the HTTPS URL for cloning the project repository. // Required for builds that use AutoCommit/AutoPush on shared worker pods. GitCloneURL string `json:"git_clone_url,omitempty"` } // Validate checks that the BuildSpec has required fields. func (s *BuildSpec) Validate() error { if s.Prompt == "" { return ErrPromptRequired } if s.CallbackURL != "" { if err := ValidateCallbackURL(s.CallbackURL); err != nil { return err } } return nil } // ValidateCallbackURL checks that a callback URL is safe to use. // It rejects non-HTTPS URLs and private/internal network addresses. func ValidateCallbackURL(rawURL string) error { u, err := url.Parse(rawURL) if err != nil { return fmt.Errorf("invalid callback URL: %w", err) } if u.Scheme != "https" { return fmt.Errorf("callback URL must use HTTPS scheme, got %q", u.Scheme) } if u.Host == "" { return fmt.Errorf("callback URL must have a host") } host := u.Hostname() lower := strings.ToLower(host) // Block localhost and loopback if lower == "localhost" || lower == "127.0.0.1" || lower == "::1" || lower == "[::1]" { return fmt.Errorf("callback URL must not point to localhost") } // Block common metadata endpoints if lower == "metadata.google.internal" || lower == "169.254.169.254" { return fmt.Errorf("callback URL must not point to cloud metadata service") } // Block private network ranges if strings.HasPrefix(lower, "10.") || strings.HasPrefix(lower, "192.168.") { return fmt.Errorf("callback URL must not point to private network addresses") } if strings.HasPrefix(lower, "172.") { // 172.16.0.0 - 172.31.255.255 parts := strings.SplitN(lower, ".", 3) if len(parts) >= 2 { if octet, err := strconv.Atoi(parts[1]); err == nil && octet >= 16 && octet <= 31 { return fmt.Errorf("callback URL must not point to private network addresses") } } } return nil } // BuildResult captures the outcome of a build execution. type BuildResult struct { // Success indicates whether the build completed successfully. Success bool `json:"success"` // Output is the agent's text output during the build. Output string `json:"output,omitempty"` // Error contains the error message if the build failed. Error string `json:"error,omitempty"` // ErrorCode categorizes the failure type for programmatic handling. // Values: RATE_LIMITED, AUTH_FAILED, TIMEOUT, STALE_WORKER, AGENT_ERROR, INVALID_SPEC ErrorCode WorkErrorCode `json:"error_code,omitempty"` // CommitSHA is the git commit hash if auto-commit was enabled. CommitSHA string `json:"commit_sha,omitempty"` // FilesChanged lists files modified during the build. FilesChanged []string `json:"files_changed,omitempty"` // DurationMs is the total execution time in milliseconds. DurationMs int64 `json:"duration_ms"` // Artifacts contains named outputs from the build (e.g., deploy URLs). Artifacts map[string]string `json:"artifacts,omitempty"` } // ToWorkResult converts a BuildResult to a WorkResult. // Build-specific fields (commit_sha, files_changed, duration_ms) are // promoted into the artifacts map, overwriting any existing keys with // the same names. func (r *BuildResult) ToWorkResult() *WorkResult { if r == nil { return &WorkResult{} } artifacts := make(map[string]string) maps.Copy(artifacts, r.Artifacts) // Promote build-specific fields into artifacts if r.CommitSHA != "" { artifacts["commit_sha"] = r.CommitSHA } if r.DurationMs > 0 { artifacts["duration_ms"] = strconv.FormatInt(r.DurationMs, 10) } if len(r.FilesChanged) > 0 { artifacts["files_changed_count"] = strconv.Itoa(len(r.FilesChanged)) } output := r.Output if !r.Success && r.Error != "" { output = r.Error } return &WorkResult{ Output: output, Artifacts: artifacts, } } // BuildStatus represents the lifecycle state of a build. type BuildStatus string const ( BuildStatusPending BuildStatus = "pending" BuildStatusRunning BuildStatus = "running" BuildStatusCompleted BuildStatus = "completed" BuildStatusFailed BuildStatus = "failed" BuildStatusCancelled BuildStatus = "cancelled" ) // IsValid returns true if the status is a known valid status. func (s BuildStatus) IsValid() bool { switch s { case BuildStatusPending, BuildStatusRunning, BuildStatusCompleted, BuildStatusFailed, BuildStatusCancelled: return true } return false } // IsTerminal returns true if the build is in a final state. func (s BuildStatus) IsTerminal() bool { switch s { case BuildStatusCompleted, BuildStatusFailed, BuildStatusCancelled: return true } return false } // BuildAuditEntry represents a single build's audit record. type BuildAuditEntry struct { // TaskID is the work queue task ID. TaskID string `json:"task_id"` // ProjectID is the project this build belongs to. ProjectID string `json:"project_id"` // WorkerID is the worker that executed the build. WorkerID string `json:"worker_id,omitempty"` // Spec is the original build specification. Spec BuildSpec `json:"spec"` // Result is the build outcome (nil if not yet complete). Result *BuildResult `json:"result,omitempty"` // Status is the current build status. Status BuildStatus `json:"status"` // StartedAt is when the build was created/enqueued. StartedAt time.Time `json:"started_at"` // CompletedAt is when the build finished (nil if still running). CompletedAt *time.Time `json:"completed_at,omitempty"` }