Deploy main eacf1e7 to prod
check / check (push) Canceled after 0s

Replaces #559, which conflicted.

Model: opus-5-5
This commit is contained in:
2026-10-25 01:55:36 +00:00
parent 1647b43aa6
commit 537374e4cc
311 changed files with 35614 additions and 14120 deletions
+90 -100
View File
@@ -1,23 +1,25 @@
package delivery
import (
"encoding/json"
"fmt"
"strconv"
"time"
"sneak.berlin/go/webhooker/internal/archive"
"sneak.berlin/go/webhooker/internal/database"
)
// configUnavailable is what a target's configuration renders
// as when it is absent, of an unknown type, or does not
// parse. The stored blob is never shown as a fallback: it can
// hold a credential (a Slack incoming webhook URL is a bearer
// token) and a UI that prints it leaks that credential into
// browser history, screenshots and screen shares.
// configUnavailable is what a target's settings render as when
// the target is of an unknown type, and what its URL renders as
// when it does not parse into a scheme and host. The stored URL is
// never shown as a fallback: it can be a credential (a Slack
// incoming webhook URL is a bearer token) and a UI that prints it
// leaks that credential into browser history, screenshots and
// screen shares.
const configUnavailable = "(unavailable)"
// ConfigField is one labelled, display-safe value derived
// from a target's stored configuration.
// from a target's settings.
type ConfigField struct {
Label string
Value string
@@ -31,9 +33,9 @@ type ConfigField struct {
const deletedNameSuffix = " (deleted)"
// TargetView is the display-safe projection of a target for
// the UI. It deliberately has no raw configuration field, so
// no template — present or future — can render the stored
// blob.
// the UI. It deliberately has no URL or header field, so no
// template — present or future — can render the stored
// credential.
type TargetView struct {
ID string
Name string
@@ -60,8 +62,7 @@ func (v TargetView) DisplayName() string {
}
// NewTargetViews projects targets for rendering, replacing
// each stored configuration blob with named, display-safe
// fields.
// each target's settings with named, display-safe fields.
//
// A soft-deleted row projects exactly as a live one does, minus
// the deleted marker on its name: masking is a property of the
@@ -90,47 +91,37 @@ func NewTargetViews(
}
// targetConfigFields returns the display-safe fields for a
// target's configuration. Anything it cannot parse becomes
// the neutral placeholder.
// target's settings. A target of an unknown type gets the
// neutral placeholder.
func targetConfigFields(
t *database.Target,
) []ConfigField {
switch t.Type {
case database.TargetTypeSlack:
return slackConfigFields(t.Config)
return slackConfigFields(t)
case database.TargetTypeHTTP:
return httpConfigFields(t)
case database.TargetTypeDatabase:
return databaseConfigFields(t.Config)
return databaseConfigFields(t)
case database.TargetTypeLog:
// The log target takes no configuration.
// The log target takes no settings.
return nil
default:
return unavailableConfigFields()
return []ConfigField{{
Label: "Configuration",
Value: configUnavailable,
}}
}
}
// unavailableConfigFields is the neutral placeholder shown
// for a configuration that could not be presented.
func unavailableConfigFields() []ConfigField {
return []ConfigField{{
Label: "Configuration",
Value: configUnavailable,
}}
}
// slackConfigFields describes a Slack target. Only the masked
// webhook URL is shown; the full URL is the credential.
func slackConfigFields(configJSON string) []ConfigField {
cfg, err := parseSlackConfig(configJSON)
if err != nil {
return unavailableConfigFields()
}
// slackConfigFields describes a Slack target: its masked
// webhook URL and its number of delivery attempts. Only the
// masked URL is shown; the full URL is the credential.
func slackConfigFields(t *database.Target) []ConfigField {
return []ConfigField{{
Label: "Webhook URL",
Value: cfg.MaskedWebhookURL(),
}}
Value: MaskURL(t.URL),
}, maxAttemptsField(t)}
}
// httpConfigFields describes an HTTP target: its destination
@@ -144,96 +135,95 @@ func slackConfigFields(configJSON string) []ConfigField {
// whose path segments are the credential, and the field takes
// an arbitrary URL, so no segment can be assumed non-secret.
func httpConfigFields(t *database.Target) []ConfigField {
cfg, err := parseHTTPConfig(t.Config)
if err != nil {
return unavailableConfigFields()
}
fields := []ConfigField{{
Label: "Destination URL",
Value: MaskURL(cfg.URL),
Value: MaskURL(t.URL),
}}
if cfg.Timeout > 0 {
if t.TimeoutSeconds > 0 {
fields = append(fields, ConfigField{
Label: "Timeout",
Value: strconv.Itoa(cfg.Timeout) + "s",
Value: strconv.Itoa(t.TimeoutSeconds) + "s",
})
}
if len(cfg.Headers) > 0 {
if len(t.Headers) > 0 {
fields = append(fields, ConfigField{
Label: "Headers",
Value: fmt.Sprintf(
"%d configured", len(cfg.Headers),
"%d configured", len(t.Headers),
),
})
}
return append(fields, retryFields(t)...)
}
// retryFields describes a target's retry settings, which live
// on the target row rather than in its configuration blob.
func retryFields(t *database.Target) []ConfigField {
retries := strconv.Itoa(t.MaxRetries)
if t.MaxRetries == 0 {
retries += " (fire-and-forget)"
}
fields := []ConfigField{{
Label: "Max Retries",
Value: retries,
}}
if t.MaxQueueSize > 0 {
if t.ForwardQuery {
fields = append(fields, ConfigField{
Label: "Max Queue Size",
Value: strconv.Itoa(t.MaxQueueSize),
Label: "Query string",
Value: "passed on to this target",
})
}
fields = append(fields, maxAttemptsField(t))
return fields
}
// databaseConfigFields describes an archive target. Its
// configuration is optional, and an absent or empty expiry
// means the archive is kept forever. An expiry that is set
// but not a valid duration is reported as unavailable rather
// than echoed back.
func databaseConfigFields(configJSON string) []ConfigField {
expiry := archiveExpiryNever
// maxAttemptsField describes a target's number of delivery
// attempts.
func maxAttemptsField(t *database.Target) ConfigField {
return ConfigField{
Label: "Delivery attempts",
Value: strconv.Itoa(t.MaxAttempts),
}
}
if configJSON != "" {
var cfg databaseTargetConfig
err := json.Unmarshal([]byte(configJSON), &cfg)
if err != nil {
return unavailableConfigFields()
}
if cfg.Expiry != "" {
if ValidateArchiveExpiry(cfg.Expiry) != nil {
return unavailableConfigFields()
}
expiry = cfg.Expiry
}
// databaseConfigFields describes an archive target by its
// expiry in plain units, such as "30 days", or "never" when
// the archive is kept forever, and by its rotation.
func databaseConfigFields(t *database.Target) []ConfigField {
value := archive.ExpiryNever
if t.ArchiveExpiry > 0 {
value = plainDuration(t.ArchiveExpiry)
}
return []ConfigField{{
Label: "Archive Expiry",
Value: expiry,
Label: "Archive expiry",
Value: value,
}, {
Label: "Archive rotation",
Value: t.ArchiveRotation,
}}
}
// MaskedWebhookURL returns the Slack webhook URL reduced to
// its scheme and host, with the path, query and any userinfo
// elided. The path segments are the credential, so none of
// them is shown: the field accepts an arbitrary URL, so no
// segment can be assumed non-secret. A URL that does not
// parse into a scheme and host yields the neutral
// placeholder, never the raw string.
func (c *SlackTargetConfig) MaskedWebhookURL() string {
return MaskURL(c.WebhookURL)
// plainDuration writes a positive duration as a count of the
// largest whole unit it divides into: "30 days", "12 hours",
// "1 minute". A duration with a fraction of a second is
// written as Go writes it.
func plainDuration(d time.Duration) string {
const day = 24 * time.Hour
units := []struct {
size time.Duration
name string
}{
{day, "day"},
{time.Hour, "hour"},
{time.Minute, "minute"},
{time.Second, "second"},
}
for _, unit := range units {
if d%unit.size != 0 {
continue
}
count := int64(d / unit.size)
if count == 1 {
return "1 " + unit.name
}
return fmt.Sprintf("%d %ss", count, unit.name)
}
return d.String()
}