package slack import ( "bytes" "context" "fmt" "net/http" "atlas9.dev/c/demo/lib/access" ) // Sender is the guarded egress layer for outbound Slack messages, the analog of // webhooks.Sender: it enforces the egress cap and POSTs the message body, so // every delivery crosses one capability-checked chokepoint. Slack incoming // webhooks are unsigned — the URL is the secret — so, unlike webhooks.Sender, // there is no HMAC signature. Delivery is only ever HTTP, so it wraps an // *http.Client directly rather than an abstract transport; tests point an // endpoint's URL at an httptest.Server. type Sender struct { // Client sends the request. A nil Client uses http.DefaultClient; configure // one with a timeout so a slow endpoint can't tie up a delivery worker. Client *http.Client Guard access.Guard } // Deliver POSTs body to url, the endpoint's unsealed Slack incoming-webhook URL, // returning the response status code. It is the chokepoint: every delivery // crosses the egress boundary here, so Cap_Slack_Send is checked exactly once. func (s *Sender) Deliver(ctx context.Context, url string, body []byte) (int, error) { if err := s.Guard.System(ctx, Cap_Slack_Send); err != nil { return 0, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return 0, err } req.Header.Set("Content-Type", "application/json") client := s.Client if client == nil { client = http.DefaultClient } resp, err := client.Do(req) if err != nil { return 0, fmt.Errorf("slack: %w", err) } defer resp.Body.Close() return resp.StatusCode, nil }