Skip to content
quarry Artifact [7b91a9b835]
Login

Artifact [7b91a9b835]

Artifact 7b91a9b835e6a6b527dc99937eb614f1fb4ce554b96f865bfd31d0ba5b4e2ca0:


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
}