golib/auth/xhubsig/errors.go
AJ ONeal 4abac2a0df
feat(auth/xhubsig): X-Hub-Signature HMAC webhook verification + HTTP middleware
Verify X-Hub-Signature-256 (and SHA-1) webhook signatures. Middleware
buffers and re-exposes the body for downstream handlers. Errors honor
Accept header: TSV default (text/plain for browsers), JSON, CSV, or
Markdown — three fields (error, description, hint) with pseudocode hints.
2026-04-13 17:04:45 -06:00

81 lines
2.1 KiB
Go

package xhubsig
import (
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"strings"
)
type httpError struct {
Error string `json:"error"`
Description string `json:"description,omitempty"`
Hint string `json:"hint,omitempty"`
}
func (e httpError) rows() [][2]string {
rows := [][2]string{{"error", e.Error}}
if e.Description != "" {
rows = append(rows, [2]string{"description", e.Description})
}
if e.Hint != "" {
rows = append(rows, [2]string{"hint", e.Hint})
}
return rows
}
func acceptedFormat(accept string) string {
for part := range strings.SplitSeq(accept, ",") {
mt := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
switch mt {
case "application/json":
return "json"
case "text/csv":
return "csv"
case "text/markdown":
return "markdown"
case "text/html":
return "text/plain"
}
}
return "tsv"
}
// writeDelimited writes vertical key-value TSV or CSV. Newlines within
// values are collapsed to a space so agents can split on newlines reliably.
func writeDelimited(w http.ResponseWriter, httpCode int, ct string, sep rune, e httpError) {
w.Header().Set("Content-Type", ct)
w.WriteHeader(httpCode)
cw := csv.NewWriter(w)
cw.Comma = sep
cw.Write([]string{"field", "value"})
for _, row := range e.rows() {
cw.Write([]string{row[0], strings.ReplaceAll(row[1], "\n", " ")})
}
cw.Flush()
}
func serializeError(w http.ResponseWriter, r *http.Request, httpCode int, e httpError) {
switch acceptedFormat(r.Header.Get("Accept")) {
case "tsv":
writeDelimited(w, httpCode, "text/tab-separated-values", '\t', e)
case "text/plain":
writeDelimited(w, httpCode, "text/plain", '\t', e)
case "csv":
writeDelimited(w, httpCode, "text/csv", ',', e)
case "markdown":
w.Header().Set("Content-Type", "text/markdown")
w.WriteHeader(httpCode)
fmt.Fprintln(w, "| field | value |")
fmt.Fprintln(w, "| --- | --- |")
for _, row := range e.rows() {
fmt.Fprintf(w, "| %s | %s |\n", row[0], strings.ReplaceAll(row[1], "\n", " "))
}
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpCode)
json.NewEncoder(w).Encode(e)
}
}