mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 15:05:00 +00:00
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.
91 lines
2.6 KiB
Go
91 lines
2.6 KiB
Go
// Authored in 2026 by AJ ONeal <aj@therootcompany.com>, assisted by AI.
|
|
// To the extent possible under law, the author(s) have dedicated all copyright
|
|
// and related and neighboring rights to this software to the public domain
|
|
// worldwide. This software is distributed without any warranty.
|
|
//
|
|
// You should have received a copy of the CC0 Public Domain Dedication along with
|
|
// this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
|
|
//
|
|
// SPDX-License-Identifier: CC0-1.0
|
|
|
|
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)
|
|
}
|
|
}
|