Public events ingest with org API keys

Go · Go · intermediate · modification

Customers have been asking to send events without going through our collector, so this adds a public `POST /v1/events` that authenticates with the API keys orgs can already create on the dashboard. The middleware checks the `Bearer` scheme, strips it off and looks the key up by its SHA-256, the same hash the dashboard stores, so we never keep or compare raw keys. Unknown and revoked keys get a 401 with `WWW-Authenticate`, and the org from the key goes into the context, so `Ingest` itself is unchanged. Tried it locally with a key from the dashboard (202) and a revoked one (401).

Go 1.22 service, standard library only (`net/http` method-pattern `ServeMux`, `log/slog`). `Handler` (existing, wired in `main.go`) already holds `Keys KeyStore` and `Log *slog.Logger`; the dashboard's API-key page creates and revokes keys through the same store. `Ingest`, `WithOrg`, `writeJSON(w, status, v)` (sets the JSON content type, writes the status and encodes `v`) and `trustedOrgHeader` are existing code in the same package. The public load balancer does not route `/internal/`, and it applies body-size and rate limits to `/v1/` before requests reach this service.

Requirements

Files touched

--- internal/ingest/routes.go
 	// Internal collector only: /internal/ is not routed by the public load balancer.
 	mux.Handle("POST /internal/events", trustedOrgHeader(http.HandlerFunc(h.Ingest)))
+	// Public ingest for customers' own senders, authenticated by org API key.
+	mux.Handle("POST /v1/events", h.RequireAPIKey(http.HandlerFunc(h.Ingest)))
 }
 
--- internal/ingest/apikey.go
+package ingest
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"net/http"
+	"strings"
+)
+
+// RequireAPIKey authenticates a request by the org API key in its
+// Authorization header and puts the key's org into the request context.
+func (h *Handler) RequireAPIKey(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		header := r.Header.Get("Authorization")
+		if !strings.HasPrefix(header, "Bearer ") {
+			unauthorized(w)
+			return
+		}
+		key := strings.TrimLeft(header, "Bearer ")
+
+		sum := sha256.Sum256([]byte(key))
+		orgID, ok, err := h.Keys.OrgByKeyHash(r.Context(), hex.EncodeToString(sum[:]))

Review this PR

Go practice