// Package workperform is the app-side client of the Work Performer Service // (§5.2) — fully independent from the atomizer: separate base URL, token, // and circuit breaker. package workperform import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "time" "bountyboard/internal/extsvc" ) type Attachment struct { Name string `json:"name"` URL string `json:"url"` MimeType string `json:"mimeType"` } type JobContext struct { RepositoryURL string `json:"repositoryUrl,omitempty"` Branch string `json:"branch,omitempty"` Instructions string `json:"instructions,omitempty"` } type JobRequest struct { TaskID string `json:"taskId"` Title string `json:"title"` Description string `json:"description"` AcceptanceCriteria []string `json:"acceptanceCriteria"` Attachments []Attachment `json:"attachments"` Links []string `json:"links"` Context JobContext `json:"context"` CallbackURL string `json:"callbackUrl"` } type JobAccepted struct { JobID string `json:"jobId"` Status string `json:"status"` } type JobStatus struct { JobID string `json:"jobId"` Status string `json:"status"` // queued | running | succeeded | failed StartedAt string `json:"startedAt"` FinishedAt string `json:"finishedAt"` } // Callback is the §5.2 service→app result payload. type Callback struct { JobID string `json:"jobId"` TaskID string `json:"taskId"` Status string `json:"status"` // succeeded | failed Summary string `json:"summary"` Artifacts []struct { Name string `json:"name"` URL string `json:"url"` } `json:"artifacts"` Log string `json:"log"` } type Client struct { c *extsvc.Client token string } func New(baseURL func() string, token string, timeout time.Duration) *Client { return &Client{c: extsvc.NewClient("work-performer", baseURL, token, timeout), token: token} } func (w *Client) BreakerState() string { return w.c.BreakerState() } func (w *Client) Healthy(ctx context.Context) error { return w.c.Healthy(ctx) } func (w *Client) Submit(ctx context.Context, req JobRequest) (*JobAccepted, error) { var out JobAccepted if err := w.c.Call(ctx, http.MethodPost, "/v1/jobs", req, &out); err != nil { return nil, err } return &out, nil } func (w *Client) Status(ctx context.Context, jobID string) (*JobStatus, error) { var out JobStatus if err := w.c.Call(ctx, http.MethodGet, "/v1/jobs/"+jobID, nil, &out); err != nil { return nil, err } return &out, nil } // Cancel is best effort (§5.2). func (w *Client) Cancel(ctx context.Context, jobID string) error { return w.c.Call(ctx, http.MethodDelete, "/v1/jobs/"+jobID, nil, nil) } // VerifySignature checks the §5.2 callback HMAC: // X-Signature = hex(hmac-sha256(body, WORK_PERFORMER_TOKEN)). func VerifySignature(token string, body []byte, signature string) bool { mac := hmac.New(sha256.New, []byte(token)) mac.Write(body) want := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(want), []byte(signature)) } // Sign produces the callback signature (used by tests and the mock service). func Sign(token string, body []byte) string { mac := hmac.New(sha256.New, []byte(token)) mac.Write(body) return hex.EncodeToString(mac.Sum(nil)) }