Help us shape the future of Go! Take the 2026 Go Developer Survey

middleware

package
v1.0.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package middleware provides the commodity HTTP middlewares and Connect interceptors that every forge-generated service ships with.

Why a library

These used to be scaffolded into each project's pkg/middleware as static files. Field evidence across downstream repos showed the copies stayed byte-identical to the templates — zero project intent, pure photocopies of security-relevant code that drifted and never received fixes. The code now lives here, versioned with forge, and the project keeps ONE thin pkg/middleware file wiring the things projects actually customize: the auth validator, the identity enricher, the unauthenticated allow-list, and dev-claims behaviour (see forge/pkg/authn for that policy surface).

What's here

HTTP middlewares (outermost layer, wrap the whole mux):

  • CORSMiddleware — spec-correct CORS with wildcard/credentials guard rails.
  • SecurityHeadersMiddleware — OWASP security response headers (CSP, nosniff, Referrer-Policy, Permissions-Policy, HSTS).
  • RequestIDMiddleware — per-request correlation ID, trusted from the inbound X-Request-Id or minted fresh; shares pkg/observe's context key so Connect log records inherit the ID.
  • HTTPStack — recovery + logging + audit for plain HTTP routes (webhooks, OAuth callbacks, REST) that bypass the Connect chain.
  • HTTPAuth — Bearer-token auth for plain HTTP routes.
  • IdempotencyMiddleware — HTTP-level Idempotency-Key replay.

Connect interceptors (project chain, after serverkit's canonical observe.DefaultMiddlewares):

Plus Redact, a struct→map helper for logging payloads without leaking PII.

Claims access

Several middlewares read the authenticated principal. The claims CONTEXT KEY is owned by the project (its pkg/middleware), so anything claims-aware takes the project's ClaimsFromContext as a callback. Passing nil degrades gracefully (anonymous audit entries, IP-keyed rate limits).

The authentication MECHANISM lives in forge/pkg/authn. Access-control decisions and row scoping stay handler logic the project writes (its own ClaimsFromContext helpers + WHERE clauses).

Index

Constants

View Source
const DefaultWebhookMaxBodySize int64 = 1 << 20 // 1MiB

DefaultWebhookMaxBodySize bounds how much of a webhook payload a handler reads. Webhook senders are unauthenticated until the signature is checked — and the signature is computed over the body — so the read has to be bounded BEFORE anything is trusted:

body, err := io.ReadAll(io.LimitReader(r.Body, middleware.DefaultWebhookMaxBodySize))
View Source
const IdempotencyKeyHeader = "Idempotency-Key"

IdempotencyKeyHeader is the canonical header carrying the client-supplied idempotency key. Methods annotated with idempotency_key=true in the proto options SHOULD require this header; if absent the request proceeds without deduplication.

View Source
const RequestIDHeader = observe.RequestIDHeader

RequestIDHeader is the canonical header carrying the per-request correlation ID on both the inbound request and the outbound response. It mirrors observe.RequestIDHeader so the HTTP layer (this middleware) and the Connect layer (observe's interceptors) stay in agreement.

Variables

View Source
var DefaultCORSAllowHeaders = []string{
	"Content-Type", "Connect-Protocol-Version", "Connect-Timeout-Ms",
	"Authorization", "Traceparent", "Tracestate", "Baggage", "X-Request-Id", "Forge-Via",
}

DefaultCORSAllowHeaders are the request headers every forge server admits cross-origin: Connect's protocol headers, the bearer token, the trace context and request id the generated clients attach, and Forge-Via, which names the client to the activity stream (forge/pkg/activity.ViaHeader).

The trace context is all three W3C headers the browser SDK's propagator can write — traceparent, tracestate AND baggage (@reliantlabs/forge-web-runtime installs HyperDX, whose propagator is trace-context plus baggage). A header missing here does not lose one trace: the preflight refuses it, and the browser then refuses EVERY cross-origin RPC that carries it.

View Source
var DefaultCORSExposeHeaders = []string{"Connect-Protocol-Version", svcerr.ReasonHeader, accesstoken.ScopesHeader, observe.TraceResponseHeader}

DefaultCORSExposeHeaders are the response headers every forge server lets a cross-origin client read: Connect's protocol version, the forge error-reason header the generated frontend runtime keys typed errors on, the scopes a scope_missing refusal names (accesstoken.ScopesHeader), and traceresponse, which names the server span the runtime stamps on errors and reports.

Functions

func AuditInterceptor

func AuditInterceptor(logger *slog.Logger, claimsFrom ClaimsLookup) connect.Interceptor

AuditInterceptor creates a Connect interceptor that produces audit log entries for every RPC call. Audit logs capture: who made the call (user ID — the email goes to the durable sink only, never to slog), what procedure was called, when, the result (success/error code), the duration, and the OTel trace id when the request carries a span.

Audit records are emitted with log_type=audit on a child logger, so they can be routed to a dedicated audit sink (separate file, SIEM, compliance database) independent of operational logs. The record message is "audit.event" ([auditMessage]) — INFO on success, WARN on error.

claimsFrom is the project's ClaimsFromContext (the project owns the claims context key — see ClaimsLookup). When nil, or when no claims are present (unauthenticated request), the user is logged as "anonymous".

This is the slog-only constructor; it is fully backward compatible. For compliance-grade durable persistence (so projects can delete a hand-rolled DB-persisting interceptor), use AuditInterceptorWithSink to additionally fan each event out to an AuditSink.

func AuditInterceptorWithSink

func AuditInterceptorWithSink(logger *slog.Logger, claimsFrom ClaimsLookup, sink AuditSink) connect.Interceptor

AuditInterceptorWithSink is AuditInterceptor plus a durable sink. Every RPC is logged to slog exactly as the slog-only variant does AND dispatched to sink.Record on a fresh bounded-timeout context off the request path, so the sink never blocks the RPC response. Passing a nil sink yields behavior identical to AuditInterceptor.

func CORSMiddleware

func CORSMiddleware(allowOrigins []string, allowCredentials bool, opts ...CORSOption) func(http.Handler) http.Handler

CORSMiddleware returns an HTTP middleware that applies the CORS policy described by allowOrigins and allowCredentials.

Semantics (per the Fetch / CORS specs):

  • When the request has no Origin header, no CORS headers are written and the request is passed through. Emitting ACAO with an empty value (or "*") on a same-origin request is a well-known footgun: the browser never needed CORS on it in the first place, and some caches key on the ACAO value.

  • When allowOrigins contains "*" AND allowCredentials is true, this middleware panics. The combination is spec-invalid (browsers reject it) and must be caught at config validation time (the scaffolded config.Validate does). The panic is a belt-and- suspenders guard if validation is bypassed.

  • When allowOrigins contains "*" and credentials are disabled, the request Origin is echoed back rather than a literal "*". Echoing keeps the response compatible with future credentialed callers (no silent breakage) and still satisfies "*" semantically.

  • When specific origins are listed, an exact (case-insensitive) match on the Origin header is required. No match → no CORS headers (the browser will block the response).

Vary: Origin is always set so intermediate caches key on the Origin.

The allowed request headers and the exposed response headers default to what Connect, forge's frontend runtime and tracing need (DefaultCORSAllowHeaders, DefaultCORSExposeHeaders). A project whose client sends or reads more extends them with WithAllowHeaders / WithExposeHeaders — see CORSOption for why that has to be the middleware's job.

func ContextWithRequestID

func ContextWithRequestID(ctx context.Context, id string) context.Context

ContextWithRequestID returns a copy of ctx with the given request ID attached. Exposed so tests and non-HTTP entrypoints can propagate the value into code paths that read RequestIDFromContext.

func DevCORSMiddleware

func DevCORSMiddleware(allowCredentials bool, opts ...CORSOption) func(http.Handler) http.Handler

DevCORSMiddleware returns an HTTP middleware that reflects ANY request Origin back to the caller — an allow-all CORS policy intended for LOCAL DEVELOPMENT ONLY. It exists so a freshly-forged product's browser frontend works cross-origin out of the box, without the author first hand-curating a CORS_ORIGINS allow-list.

Why this instead of passing "*" to CORSMiddleware:

  • Credential-safe. It always echoes the CONCRETE request Origin into Access-Control-Allow-Origin (never the literal "*"), so pairing it with Access-Control-Allow-Credentials: true stays spec-valid. The "*" path in CORSMiddleware, by contrast, panics when credentials are enabled (the wildcard+credentials combination is spec-invalid and rejected in prod).

  • Unambiguously dev-scoped. The generated server selects it ONLY when ENVIRONMENT=development, so the permissive behavior can never leak into a production build the way a stray "*" in a prod allow-list could.

It MUST NOT be wired outside development. Reflecting every origin (especially with credentials) is exactly the footgun CORSMiddleware guards against for real deployments.

It takes the same header options as CORSMiddleware and applies them the same way. Reflecting whatever headers a dev request asks for would make dev pass a header that production then refuses — the exact gap that let a web client header take a deployed app down — so dev and production allow one list.

func HTTPAuth

func HTTPAuth(
	authenticate func(token string) (*auth.Claims, error),
	withClaims func(ctx context.Context, claims *auth.Claims) context.Context,
) func(http.Handler) http.Handler

HTTPAuth returns HTTP middleware that validates Bearer tokens and populates the context with claims, mirroring the Connect auth interceptor for plain HTTP routes.

authenticate validates the raw token and returns the claims — pass the project's ValidateToken. withClaims stashes them on the context — pass the project's ContextWithClaims (the project owns the claims context key). If authenticate is nil (dev mode), all requests are allowed through untouched.

func HTTPStack

func HTTPStack(logger *slog.Logger, claimsFrom ClaimsLookup) func(http.Handler) http.Handler

HTTPStack returns HTTP middleware that applies recovery, logging, and audit to plain HTTP handlers. This provides the same cross-cutting concerns as the Connect interceptors for routes that cannot use the Connect protocol (e.g. webhooks, OAuth callbacks, REST endpoints).

claimsFrom is the project's ClaimsFromContext (see ClaimsLookup); nil produces anonymous audit entries.

Auth is NOT included because REST routes often have different auth requirements (e.g. webhook signature verification instead of JWT). Use HTTPAuth separately for routes that need Bearer authentication.

func HeaderList

func HeaderList(csv string) []string

HeaderList splits a comma-separated header list (a config value) into names, dropping blanks. For WithAllowHeaders / WithExposeHeaders.

func IdempotencyInterceptor

func IdempotencyInterceptor(opts IdempotencyOptions) connect.Interceptor

IdempotencyInterceptor returns a Connect interceptor that deduplicates requests carrying an Idempotency-Key header. If the same key is seen within the TTL window, the cached response (or error) is returned without calling the handler again.

Keys are scoped per-procedure so the same key on different RPCs is treated as distinct. For the HTTP-layer equivalent (REST/webhook routes) see IdempotencyMiddleware.

When opts.CacheSize <= 0 the interceptor is disabled and nil is returned.

func IdempotencyMiddleware

func IdempotencyMiddleware(cfg IdempotencyConfig) func(http.Handler) http.Handler

IdempotencyMiddleware returns an http.Handler that deduplicates requests carrying the same key. If a request with that key has already been served (and is still cached), the cached response is replayed without invoking the downstream handler.

By default the key is the Idempotency-Key header and every completed response is cached; IdempotencyConfig.Key and IdempotencyConfig.ShouldCache override each half independently.

Usage:

mux := http.NewServeMux()
cfg := middleware.IdempotencyConfig{CacheSize: 500, TTL: 30 * time.Minute}
handler := middleware.IdempotencyMiddleware(cfg)(mux)

The cache is bounded (LRU over CacheSize entries) and PER-PROCESS: two replicas each keep their own, so a duplicate routed to the other replica is served fresh. Replay also happens BEFORE the wrapped handler runs, so a route that authenticates inside the handler (a webhook verifying its signature) answers unauthenticated duplicates from the cache. When either property matters, deduplicate inside the handler instead — see DedupeStore for the boolean-shaped seam that swaps for a table.

func RateLimitInterceptor

func RateLimitInterceptor(opts RateLimitOptions, claimsFrom ClaimsLookup) connect.Interceptor

RateLimitInterceptor returns a Connect interceptor that enforces a per-key token-bucket rate limit. Keys are derived in this order:

  1. authenticated claim subject (claims.UserID) via claimsFrom, if available
  2. peer IP from the Connect request/stream peer address

When opts.Rps <= 0 the interceptor is disabled and this function returns nil. Memory is bounded by an LRU cache of up to rateLimitCacheSize limiters; idle keys are evicted in LRU order so the interceptor is safe to expose to anonymous traffic. claimsFrom may be nil (all callers are then keyed by peer IP).

func Redact

func Redact(v any, fields ...string) map[string]any

Redact accepts a struct (or pointer to struct) and a set of field names to mask. It returns a map[string]any mirroring the struct's exported fields, with the named fields replaced by "[REDACTED]".

Non-struct inputs return nil. Unexported fields are silently skipped.

Example:

type User struct {
    ID    string
    Email string
    Name  string
}
out := middleware.Redact(User{ID: "1", Email: "a@b.c", Name: "Alice"}, "Email")
// out == map[string]any{"ID": "1", "Email": "[REDACTED]", "Name": "Alice"}

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request ID attached to ctx, or "" if none is set. Useful for handlers and downstream logging sites that want to include the ID on custom log lines. Nil ctx returns "" (never panics) so callers in CLI/worker contexts can share the same helper.

The storage is pkg/observe's context key: a request ID set by this HTTP middleware is visible to observe.LoggingInterceptor (and vice versa) without a header round-trip.

func RequestIDMiddleware

func RequestIDMiddleware() func(http.Handler) http.Handler

RequestIDMiddleware is an HTTP middleware that ensures every request carries a correlation ID. It must be wired ahead of the logging middleware so log records inherit the generated ID.

Behavior:

  • If the inbound request has a non-empty RequestIDHeader, that value is trusted and reused. This lets edge proxies and upstream services stitch together a single trace across hops without coordination.

  • Otherwise a 16-byte crypto/rand hex token is minted — the same shape observe.RequestIDInterceptor mints at the Connect layer, so IDs look uniform whichever layer assigned them. crypto/rand keeps IDs unpredictable to third parties that might otherwise enumerate them.

  • The chosen ID is exposed to downstream middleware/handlers via the request context (RequestIDFromContext), stamped back onto the INBOUND request header, and echoed on the response header so the client can log it for later correlation.

This middleware OWNS the response echo for every request that passes through it. observe.RequestIDInterceptor, which runs inside it on the Connect path, adopts the context value and deliberately writes no response header of its own — otherwise connect merges its header into the one already written here and the response carries two conflicting X-Request-Id values.

The inbound stamp exists for the same reason: handlers and interceptors that reach for the request header rather than the context must see the SAME id, not an empty string that invites them to mint another.

func SecurityHeadersMiddleware

func SecurityHeadersMiddleware(cfg SecurityHeadersConfig) func(http.Handler) http.Handler

SecurityHeadersMiddleware returns an HTTP middleware that sets OWASP-recommended security response headers:

  • Content-Security-Policy — locks down what the browser may load
  • X-Content-Type-Options: nosniff — prevents MIME sniffing
  • Referrer-Policy — limits Referer leakage
  • Permissions-Policy — opts out of powerful browser APIs
  • Strict-Transport-Security — forces HTTPS (production only)

Headers relevant to health/pprof endpoints (health checks don't need CSP stringency, but we still set nosniff/referrer/permissions there for consistency and defense in depth). Callers wire this in at the outermost HTTP layer so every response benefits — including /healthz, /readyz, /metrics, and REST/webhook routes that bypass the Connect interceptor chain.

When cfg.Enabled is false, this becomes a no-op.

func VerifyHMACSHA256

func VerifyHMACSHA256(body []byte, signature, secret string) bool

VerifyHMACSHA256 reports whether signature is a valid HMAC-SHA256 of body under secret. signature is hex, with or without the "sha256=" prefix some providers (GitHub) prepend, in either letter case.

The comparison is constant-time, and a malformed signature is a plain false rather than an error, so call sites stay a single `if`:

if !middleware.VerifyHMACSHA256(body, r.Header.Get("X-Signature-256"), secret) {
    http.Error(w, "unauthorized", http.StatusUnauthorized)
    return
}

An empty secret always fails: an unconfigured secret must not silently turn signature verification into a no-op.

func WebhookEventID

func WebhookEventID(headers http.Header) string

WebhookEventID returns the sender's unique delivery ID from the first recognised header, or "" when none is present.

Providers do not agree on a header, and none of them use the Idempotency-Key convention that REST clients follow, so this covers the common ones: the generic X-Event-ID / X-Webhook-ID / X-Request-ID spellings plus Stripe-Event-Id and X-GitHub-Delivery. A provider that stamps something else is one header read away — pull it directly and fall back to this:

id := r.Header.Get("X-Acme-Delivery")
if id == "" {
    id = middleware.WebhookEventID(r.Header)
}

Types

type AuditEvent

type AuditEvent struct {
	// Timestamp is the wall-clock time the RPC started.
	Timestamp time.Time
	// Procedure is the fully-qualified Connect procedure (e.g.
	// "/pkg.Service/Method").
	Procedure string
	// PeerAddress is the remote peer address.
	PeerAddress string
	// UserID is the authenticated user id, or "anonymous" when no
	// claims were present.
	UserID string
	// Email is the authenticated user email, empty when anonymous.
	Email string
	// Duration is the measured RPC duration.
	Duration time.Duration
	// Status is "ok" on success or "error" on failure.
	Status string
	// ErrorCode is the connect.Code string on failure, empty on success.
	ErrorCode string
	// ErrorMessage is the error string on failure, empty on success.
	ErrorMessage string
	// TraceID is the OTel trace id (hex), empty when the request carries
	// no span context.
	TraceID string
	// RequestID is the per-request correlation id (the X-Request-Id the
	// client was handed), empty when the request reached the interceptor
	// without one. It is what makes the audit trail JOINABLE: without it
	// a customer-quoted request id finds the operational "rpc completed"
	// line but nothing in the audit record, so "what did this call
	// actually do, and who made it" cannot be answered from the id alone.
	// TraceID does not substitute — it is empty whenever no OTLP/tracing
	// span exists, which is the default configuration.
	RequestID string
}

AuditEvent is the full, structured record of a single RPC, handed to an AuditSink for durable persistence. It carries every field forge emits to slog plus the OTel trace correlation id, so a sink can write a complete compliance record without re-deriving anything from the request context.

The field set is intentionally a superset that lets a thin adapter satisfy a richer store. Status is "ok" or "error"; ErrorCode is the connect.Code string (e.g. "permission_denied") and is empty on success; TraceID is the hex OTel trace id, empty when the request carries no span.

type AuditSink

type AuditSink interface {
	Record(ctx context.Context, e AuditEvent)
}

AuditSink is a durable destination for audit events (a database, an append-only file, a SIEM forwarder). The interceptor calls [Record] for every RPC AFTER it has emitted the slog record.

Record is invoked on a fresh, bounded-timeout context off the request path — the interceptor never blocks the RPC response on the sink. A sink implementation may therefore treat Record as synchronous: do its write, honor ctx cancellation, and surface its own errors via its own logger. Record has no error return precisely so the interceptor stays fire-and-forget; the sink owns failure handling.

A thin adapter satisfies AuditSink over any richer store. For example, control-plane's audit store has the signature:

// pkg/audit.Store
Log(ctx context.Context, entry Entry) error

which an adapter wraps directly:

type storeSink struct{ store pkgaudit.Store; log *slog.Logger }
func (s storeSink) Record(ctx context.Context, e middleware.AuditEvent) {
    entry := pkgaudit.Entry{
        Timestamp:    e.Timestamp,
        Procedure:    e.Procedure,
        PeerAddress:  e.PeerAddress,
        UserID:       e.UserID,
        Email:        e.Email,
        DurationMs:   int(e.Duration.Milliseconds()),
        Status:       e.Status,
        ErrorCode:    e.ErrorCode,
        ErrorMessage: e.ErrorMessage,
        Metadata:     map[string]string{"trace_id": e.TraceID},
    }
    if err := s.store.Log(ctx, entry); err != nil {
        s.log.Error("audit db write failed", "error", err, "procedure", e.Procedure)
    }
}

type CORSOption

type CORSOption func(*corsHeaderLists)

CORSOption extends a CORS policy beyond the defaults.

WHY THE MIDDLEWARE HAS TO OFFER THIS. The preflight is answered HERE, before any of the project's own middleware runs, so a project could not add a header to it from the outside: a web client header that is not on the list makes the browser refuse every RPC, and the only remedy was to stop sending the header. A project declares its extra headers instead (the scaffolded cors_allow_headers / cors_expose_headers config), and the defaults stay.

func WithAllowHeaders

func WithAllowHeaders(headers ...string) CORSOption

WithAllowHeaders admits more request headers in the preflight.

func WithExposeHeaders

func WithExposeHeaders(headers ...string) CORSOption

WithExposeHeaders lets a cross-origin client read more response headers.

type ClaimsLookup

type ClaimsLookup func(ctx context.Context) (*auth.Claims, bool)

ClaimsLookup resolves the authenticated principal from a request context. The claims context key is owned by the project's pkg/middleware, so claims-aware interceptors in this library take the project's ClaimsFromContext as a callback. nil is allowed and means "no claims available" (the interceptors degrade gracefully).

type DedupeConfig

type DedupeConfig struct {
	// MaxKeys bounds how many keys are remembered at once. The oldest
	// (least recently touched) key is dropped when the bound is reached,
	// so a flood of unique keys can never grow the process's memory.
	// Default: 1000.
	MaxKeys int

	// TTL is how long a marked key stays marked. Size it to the sender's
	// retry window: a provider that retries a failed delivery for 24h
	// needs a 24h TTL to still recognise the redelivery as a duplicate.
	// Default: 1 hour.
	TTL time.Duration
}

DedupeConfig configures a DedupeStore. The zero value is a working default: 1000 keys, 1h TTL.

type DedupeStore

type DedupeStore struct {
	// contains filtered or unexported fields
}

DedupeStore answers "have I already handled this?" for a stream of opaque keys. It is the boolean-shaped counterpart to IdempotencyMiddleware: the middleware replays a cached RESPONSE from in front of a handler, a DedupeStore lets the handler itself decide, which is what you want when the check has to happen after some other gate (verifying a webhook signature) or when the answer must be marked only once processing actually succeeded.

if store.Seen(eventID) { return }   // already handled — ack and stop
if err := process(evt); err != nil { return err }
store.Mark(eventID)                 // only after success, so a retry
                                    // of a FAILED delivery re-runs

The store is bounded and PER-PROCESS. Two replicas each keep their own, so a duplicate delivery routed to the other replica is processed again. When at-most-once has to hold across replicas, keep the same call shape and back it with shared storage — a table with a unique constraint on the key, where Seen is a SELECT and Mark an INSERT.

Safe for concurrent use. Seen and Mark are separate operations, so two deliveries of the same key arriving at once can both observe "not seen" and both process; a unique constraint in shared storage is what closes that window for good.

func NewDedupeStore

func NewDedupeStore(cfg DedupeConfig) *DedupeStore

NewDedupeStore returns a bounded, TTL'd key set. See DedupeConfig for the defaults applied to zero fields.

func (*DedupeStore) Len

func (s *DedupeStore) Len() int

Len returns how many keys are currently tracked, including any whose TTL has lapsed but that eviction has not yet reclaimed. Exposed for tests and diagnostics that assert the bound holds.

func (*DedupeStore) Mark

func (s *DedupeStore) Mark(key string)

Mark records key as handled for the configured TTL. Marking a key that is already present refreshes its expiry. An empty key is ignored.

func (*DedupeStore) Seen

func (s *DedupeStore) Seen(key string) bool

Seen reports whether key was marked within the TTL window. An empty key is never seen — callers that cannot derive a key must fall through to processing rather than silently dropping the work.

type IdempotencyConfig

type IdempotencyConfig struct {
	// CacheSize is the maximum number of idempotency keys to track.
	// Default: 1000.
	CacheSize int

	// TTL is how long a cached response is kept. After this duration the
	// key is evicted and a duplicate request is treated as new.
	// Default: 1 hour.
	TTL time.Duration

	// Key derives the deduplication key from the inbound request.
	// Returning "" opts that request out of deduplication entirely: it is
	// passed straight through and its response is never cached.
	//
	// nil reads the IdempotencyKeyHeader — the client-driven convention
	// REST/RPC callers follow. Supply a Key when the sender does not
	// control that header: webhook providers each stamp their own event ID
	// (Stripe-Event-Id, X-GitHub-Delivery, …), and callback routes may key
	// off a path value.
	//
	//	cfg.Key = func(r *http.Request) string {
	//	    return middleware.WebhookEventID(r.Header)
	//	}
	Key func(*http.Request) string

	// ShouldCache decides, from the status code the handler produced,
	// whether that response is stored for replay. nil caches every
	// completed response.
	//
	// Supply a predicate whenever the sender RETRIES on failure. Caching a
	// 500 replays that 500 for the whole TTL, so every retry the sender
	// makes is answered from the cache and the request is never actually
	// reprocessed:
	//
	//	cfg.ShouldCache = func(status int) bool { return status < 400 }
	ShouldCache func(statusCode int) bool
}

IdempotencyConfig controls the behavior of the idempotency middleware.

The zero value is a working default: 1000 keys, 1h TTL, keyed off the Idempotency-Key header, caching every completed response.

type IdempotencyOptions

type IdempotencyOptions struct {
	CacheSize int
	TTL       time.Duration
}

IdempotencyOptions configures the RPC idempotency interceptor.

  • CacheSize: maximum number of cached responses (default 1000)
  • TTL: how long a cached response is valid (default 1h)

CacheSize <= 0 disables the interceptor — IdempotencyInterceptor returns nil and callers should skip appending it to the chain.

type RateLimitOptions

type RateLimitOptions struct {
	Rps   int
	Burst int
}

RateLimitOptions tunes the per-key token-bucket rate limiter.

  • Rps: steady-state requests-per-second per key
  • Burst: maximum burst allowed before throttling (raised to Rps when lower)

Rps <= 0 disables rate limiting — RateLimitInterceptor returns nil in that case and callers should skip appending the interceptor to the chain.

type SecurityHeadersConfig

type SecurityHeadersConfig struct {
	// Enabled disables the middleware entirely when false. Allows operators
	// to opt out without unwinding the handler chain.
	Enabled bool

	// Production toggles headers that only make sense when served over
	// HTTPS in a real deployment (currently: Strict-Transport-Security).
	// Typically mirrors cfg.Environment != "development".
	Production bool

	// ContentSecurityPolicy overrides the default CSP. The default is
	// "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" which
	// is appropriate for an API server that does not serve HTML. If the
	// server also serves HTML (e.g. an admin UI), supply a CSP that
	// permits the required sources explicitly.
	ContentSecurityPolicy string

	// PermissionsPolicy overrides the default Permissions-Policy. The
	// default denies access to camera, microphone, geolocation,
	// interest-cohort (FLoC), payment, and USB.
	PermissionsPolicy string

	// ReferrerPolicy overrides the default Referrer-Policy
	// ("strict-origin-when-cross-origin").
	ReferrerPolicy string

	// HSTSMaxAge overrides the default Strict-Transport-Security max-age
	// (2 years, per MDN / OWASP guidance). Only emitted when Production is
	// true. Set to a negative value to disable HSTS even in production.
	HSTSMaxAge int
}

SecurityHeadersConfig controls which headers SecurityHeadersMiddleware sets and the exact values used. Zero-value fields fall back to sensible OWASP defaults suitable for a Connect/HTTP API server.

func DefaultSecurityHeadersConfig

func DefaultSecurityHeadersConfig() SecurityHeadersConfig

DefaultSecurityHeadersConfig returns a SecurityHeadersConfig with OWASP defaults suitable for an API server. Callers should toggle Enabled and Production based on their runtime config.

type WebhookEvent

type WebhookEvent struct {
	// ID uniquely identifies this delivery and is the deduplication key
	// (see WebhookEventID and DedupeStore).
	ID string
	// Source names the webhook the delivery arrived on.
	Source string
	// Headers are the inbound request headers.
	Headers http.Header
	// Body is the raw, size-limited request body the signature covered.
	Body []byte
	// ReceivedAt is when the delivery was accepted.
	ReceivedAt time.Time
}

WebhookEvent is one received webhook delivery, assembled by a handler after the signature checks out and handed to the processing function. It carries the raw body rather than a parsed payload because the shape is provider-specific — unmarshal Body into your own type.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL