package notify
import (
"context"
"encoding/json"
"fmt"
)
// ReplyEndpoint is notify's record of where it delivers the forge's
// reply events (RFC 0014 Phase 1). Secret is set only on the call that
// minted it — creation or rotation — because notify shows it once.
type ReplyEndpoint struct {
ID string `json:"id"`
URL string `json:"url"`
Signed bool `json:"signed"`
Secret string `json:"secret"`
}
// EnsureReplyEndpoint makes url a signed webhook endpoint on notify's
// delivery stream. An endpoint already carrying that URL and signing
// is left alone; one that exists unsigned — created before notify could
// sign — is given a secret; otherwise one is created, active. Either
// way the returned Secret is non-empty exactly when the caller has a
// new key to store: notify seals the secret at rest and never shows it
// again, so the forge's copy is the only one.
func (c *Client) EnsureReplyEndpoint(ctx context.Context, url string) (ep ReplyEndpoint, err error) {
if c == nil || c.base == "" || url == "" {
return ReplyEndpoint{}, nil
}
status, body, err := c.do(ctx, "GET", "/v1/webhooks/endpoints", nil)
if err != nil {
return ReplyEndpoint{}, fmt.Errorf("notify: list endpoints: %w", err)
}
if status >= 300 {
return ReplyEndpoint{}, fmt.Errorf("notify: list endpoints: status %d", status)
}
var have struct {
Data []ReplyEndpoint `json:"data"`
}
if err := json.Unmarshal(body, &have); err != nil {
return ReplyEndpoint{}, fmt.Errorf("notify: list endpoints: %w", err)
}
for _, e := range have.Data {
if e.URL != url {
continue
}
if e.Signed {
return e, nil
}
status, body, err := c.post(ctx, "/v1/webhooks/endpoints/"+e.ID+"/secret", nil)
if err != nil {
return ReplyEndpoint{}, fmt.Errorf("notify: sign endpoint: %w", err)
}
if status >= 300 {
return ReplyEndpoint{}, fmt.Errorf("notify: sign endpoint: status %d", status)
}
if err := json.Unmarshal(body, &e); err != nil {
return ReplyEndpoint{}, fmt.Errorf("notify: sign endpoint: %w", err)
}
e.URL, e.Signed = url, true
return e, nil
}
status, body, err = c.post(ctx, "/v1/webhooks/endpoints", map[string]any{"url": url, "active": true})
if err != nil {
return ReplyEndpoint{}, fmt.Errorf("notify: create endpoint: %w", err)
}
if status >= 300 {
return ReplyEndpoint{}, fmt.Errorf("notify: create endpoint: status %d", status)
}
if err := json.Unmarshal(body, &ep); err != nil {
return ReplyEndpoint{}, fmt.Errorf("notify: create endpoint: %w", err)
}
ep.URL = url
return ep, nil
}