stdcrpcwritefence

package
v0.0.237 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package stdcrpcwritefence gives a client read-your-writes consistency against an ro/rw transactor pair without any server-side state.

It is composed of two pieces that share a single, package-private ctx flag:

  • Middleware — an HTTP middleware that (a) verifies the inbound write-fence cookie and, on success, hands the request ctx to a caller-supplied promoter (WithReadPromotion) — typically `stdent.WithReadPromotion` — so a subsequent transact call can route the read to the rw transactor, and (b) installs a fresh fence-intent flag on the request ctx; on the way out, if anything flipped that flag, the middleware pins a freshly-signed cookie on the response BEFORE the status line is flushed.

  • Interceptor — a server-side ConnectRPC unary interceptor that flips the fence-intent flag whenever the inbound procedure's idempotency level is anything other than NO_SIDE_EFFECTS and the handler returned nil. The decision is read straight off connect.Spec.IdempotencyLevel, which is in turn driven by the procedure's `idempotency_level` proto annotation — no codegen, no bespoke annotation, and no handler-body changes required.

The package is intentionally not tied to ent: the write-detection side is purely wire-level (the interceptor) and the read-side ctx stamp is delegated to a caller-supplied hook via WithReadPromotion. The composition root supplies the concrete promoter (typically `stdent.WithReadPromotion`) at wiring time — keeping this package free of any ent / transactor dependency.

The cookie payload is opaque and trivial (a fixed string). All the trust lives in the HMAC signature and the securecookie.MaxAge timestamp embedded by securecookie: the signature prevents a client from forging or extending the window, the embedded timestamp prevents replay past the configured TTL.

Failure mode for cookie verification is fail-open: any error (cookie missing, malformed, tampered, expired) is treated as "no promotion". The middleware never short-circuits the request and never writes to the response except to add the Set-Cookie header when a write was observed.

Index

Constants

View Source
const (
	// DefaultCookieName is the cookie name used when [WithCookieName]
	// is not supplied. The leading underscore mirrors the "host-only,
	// internal" convention some browsers / proxies treat specially.
	DefaultCookieName = "_wfence"

	// DefaultTTL is the read-your-writes window applied to every
	// pinned response when [WithTTL] is not supplied. Three seconds
	// is a typical Aurora / RDS reader-lag budget; tune per
	// deployment.
	DefaultTTL = 3 * time.Second

	// MinHashKeyLen is the minimum hash key length enforced at
	// construction time. 32 bytes is the size of an HMAC-SHA-256
	// key — anything shorter weakens the signature for no benefit.
	MinHashKeyLen = 32
)

Variables

This section is empty.

Functions

func Interceptor

func Interceptor() connect.UnaryInterceptorFunc

Interceptor returns a server-side Connect unary interceptor that flips this package's fence-intent flag on the request ctx whenever the inbound procedure's idempotency level is anything other than connect.IdempotencyNoSideEffects AND the handler returned nil.

The decision is read straight off connect.Spec.IdempotencyLevel, which is in turn driven by the procedure's `idempotency_level` proto annotation — no codegen, no bespoke annotation, and no handler-body changes required.

The interceptor is a sibling of Middleware: both belong to this package because they share the unexported fence-intent flag. The middleware installs the flag; the interceptor flips it. If the middleware did not run for this request (e.g. a worker-internal call), the flip becomes a no-op — matching the fail-quiet behaviour every ctx-stamped seam in this repo follows.

Wiring: register on the inbound (server-side) Connect interceptor chain alongside the rest of the stdcrpc* interceptors. The interceptor is a no-op on the client side — fence decisions are server-side only.

Default behaviour for procedures that don't declare an idempotency level (i.e. connect.IdempotencyUnknown, the proto-default zero value) is to fence on success. This is fail-safe (the cost is at most one over-pinned read window per call) and serves as gentle pressure on API authors to mark pure reads as NO_SIDE_EFFECTS so they also pick up Connect's GET routing.

func MarkFenceIntent

func MarkFenceIntent(ctx context.Context)

MarkFenceIntent flips the fence-intent flag attached to ctx by Middleware, if any. The Interceptor is the canonical flipper and covers every Connect handler whose procedure is not annotated `idempotency_level = NO_SIDE_EFFECTS`. This helper exists for the rare case where a non-Connect code path knows it wrote and wants the same fence semantics — e.g. a hand-rolled HTTP handler that mutates state outside the Connect chain.

A no-op (one ctx lookup, type assertion fails) when no fence intent is attached, so callers outside an HTTP request scope (CLI bootstrap, Temporal activities, tests) are unaffected.

Symmetry with [stdent.WithReadPromotion]: the read side exports a public ctx stamper (stamp from anywhere, consumed by the routing helpers), the write side exports a public marker (mark from anywhere, consumed by this package's middleware).

func Middleware

func Middleware(hashKey []byte, opts ...Option) func(http.Handler) http.Handler

Middleware builds an HTTP middleware that implements cookie-based read-your-writes routing on top of this package's fence-intent flag and a caller-supplied ctx promoter (see WithReadPromotion).

hashKey is the HMAC-SHA-256 key used to sign / verify cookies. It MUST be at least MinHashKeyLen bytes; shorter keys panic at construction time (programmer error). The key is the only piece of secret material in the middleware — rotating it soft- invalidates every in-flight cookie, which is the desired behaviour during key rotation.

The returned middleware:

  1. Reads the configured cookie from the request; on a verified, unexpired cookie it hands ctx to the promoter supplied via WithReadPromotion (if any) so a subsequent transact call can open against the rw transactor instead of the ro one.

  2. Installs a fresh fence-intent flag on ctx so any caller down the stack (canonically Interceptor, optionally MarkFenceIntent) can request a cookie pin.

  3. Wraps the http.ResponseWriter so that the first call to WriteHeader or Write (whichever lands first) — or, failing either, the moment the handler returns — checks the flag and, if set, adds a freshly-signed cookie to the response headers BEFORE the status line is flushed to the wire.

Types

type Option

type Option func(*config)

Option configures a Middleware.

func WithCookieName

func WithCookieName(name string) Option

WithCookieName overrides the cookie name used by the middleware. Use this to namespace the cookie per-app when multiple apps share the same domain.

func WithDomain

func WithDomain(d string) Option

WithDomain sets the cookie's Domain attribute. Empty (the default) means host-only.

func WithInsecure

func WithInsecure() Option

WithInsecure drops the Secure attribute from the issued cookie. Use only for local development over plain HTTP; production deployments MUST keep Secure on (the default).

func WithPath

func WithPath(p string) Option

WithPath overrides the cookie's Path attribute. Defaults to "/".

func WithReadPromotion

func WithReadPromotion(promote func(context.Context) context.Context) Option

WithReadPromotion sets the ctx promoter the middleware invokes on a successful cookie verification. The promoter returns a derived ctx that downstream code (typically `stdent.TransactR` / `stdent.TransactR0`) consults to route the read to the rw transactor.

Composition roots wire this with `stdent.WithReadPromotion` (or any equivalent ctx stamp from a different routing layer). Leaving it unset means the middleware verifies the cookie but does not modify ctx — useful when the cookie is only there to drive fence-intent reasoning and routing is handled elsewhere.

Keeping the promoter behind an option is the single seam by which this package avoids a hard dependency on stdent: the read-side stamp is injected by the caller, the write-side flag is owned here.

func WithSameSite

func WithSameSite(s http.SameSite) Option

WithSameSite overrides the cookie's SameSite attribute. Defaults to http.SameSiteLaxMode which is correct for typical web apps; switch to http.SameSiteStrictMode for stricter origin pinning.

func WithTTL

func WithTTL(d time.Duration) Option

WithTTL overrides the read-your-writes window. The same duration is set both on the issued cookie's Max-Age (browser-side expiry) and on the underlying securecookie codec (server-side validation), so a client cannot extend the window by replaying an old cookie.

Jump to

Keyboard shortcuts

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