NextMyDay/internal/core/middleware/correlation.go
Claus Lohmar d11e14295b feat(core): add the store, identity plumbing and the door (SPEC-003/004)
- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded
  migration runner; each module's migrations are name-prefixed (D-012)
- audit: the append-only audit log; the actor threads via context (R-1)
- users: the user_id/GUID/email anchors (D-013)
- config: the YAML settings file (D-035) + validation
- httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security
  headers · redaction
- middleware: correlation id · audit · rate limiting
- mail: the invite/recovery mailer seam
- server: the core DOOR — the resolution switch, the dispatch registry
  (RegisterModule), the role gates, /setup · enroll/login · recovery flows,
  the reserved `static` workflow, and the loopback-only bootstrap (D-025)
2026-10-14 10:53:51 +00:00

57 lines
2 KiB
Go

// Package middleware holds the door's cross-cutting HTTP concerns — the
// correlation id, the rate limiter, and the audit-metadata attachment
// (SPEC-003 §3/§4).
//
// These are deliberately small and transport-level: the door
// (internal/core/server) orchestrates them in one visible pipeline.
package middleware
import (
"context"
"crypto/rand"
"fmt"
"net/http"
"time"
)
// CorrelationHeader is the response header carrying the correlation id, echoed
// in every log line and audit row (SPEC-003 §4/§8, AC-8).
const CorrelationHeader = "X-Correlation-ID"
// correlationKey is the unexported context key for the correlation id.
type correlationKey struct{}
// NewCorrelationID returns a fresh UUID v4 string. It is generated per request:
// an inbound header is never trusted, so a caller cannot forge a trail.
func NewCorrelationID() string {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
// crypto/rand failing is a platform fault; fall back to a
// time-derived id rather than emitting an empty correlation.
return fmt.Sprintf("corr-%d", time.Now().UnixNano())
}
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // RFC 4122 variant
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// WithCorrelation attaches id to ctx.
func WithCorrelation(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, correlationKey{}, id)
}
// CorrelationFrom returns the correlation id attached to ctx, or "".
func CorrelationFrom(ctx context.Context) string {
id, _ := ctx.Value(correlationKey{}).(string)
return id
}
// CorrelationID is the middleware form: it generates a correlation id, puts it
// on the response header and the request context, then calls next.
func CorrelationID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := NewCorrelationID()
w.Header().Set(CorrelationHeader, id)
next.ServeHTTP(w, r.WithContext(WithCorrelation(r.Context(), id)))
})
}