cli

package
v0.0.0-...-9f2d632 Latest Latest
Warning

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

Go to latest
Published: Jun 27, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Wave 4b Task 3 — Accept-header content negotiation primitive.

Pure functions over header strings + a typed media-type lookup. No Gin coupling at this layer; the Gin glue lives in commons/cli/toon.go (W4b-T4) which calls NegotiateContentType + MarshalChosen + UnmarshalChosen below.

Policy (per W4b operator decisions 1–6, plan §"Operator-locked decisions" 2026-05-22):

  • Accept: */* ⇒ server default (Herald default = TOON; operator may flip via the env var HERALD_DEFAULT_RESPONSE_CODEC=json)
  • Accept: application/toon ⇒ TOON
  • Accept: application/json ⇒ JSON
  • Accept: both with no q ⇒ TOON wins (Herald-default tie-break)
  • Accept: both with q params ⇒ higher q wins
  • Accept: missing / empty ⇒ server default
  • Content-Type: application/toon ⇒ TOON
  • Content-Type: application/json or empty ⇒ JSON (backwards-compat: curl -d '...' usage)

§107 anti-bluff anchors enforced at this layer:

  • MarshalChosen with ct="application/toon" goes through the REAL digital.vasic.toon (which wraps github.com/toon-format/toon-go since W4b-T2). The output never begins with '{' or '[' — the original 2026-05-17 PASS-bluff would have surfaced as JSON-syntax bytes here.
  • UnmarshalChosen round-trips the same bytes back into the same Go struct; "no error" alone is insufficient evidence per Universal §11.4 / Herald §107. The paired test asserts reflect.DeepEqual.
  • UnmarshalChosen with an unknown content-type returns an explicit error naming the unsupported CT — never a silent JSON fallback that would mask a misrouted body.

Constitutional anchors: Universal §11.4 (PASS-bluff prevention), Herald §107 (end-user-usability covenant), W4b operator decision 3 (HERALD_DEFAULT_RESPONSE_CODEC opt-out).

Package cli is the shared CLI scaffold for every Herald flavor binary. Per Universal §11.4.74 catalogue-check: vendored as Herald-internal (no-match against vasic-digital + HelixDevelopment).

Wave 4b Task 4 — Gin TOON encode/decode middleware.

This file is the consumer-facing seam between Herald's flavor handlers (which still write `c.JSON(status, obj)` like every other Gin app) and the content-negotiation primitive in commons/cli/contentnego.go. The middleware swaps Gin's ResponseWriter for a buffering wrapper so that, after the handler finishes, we can:

  1. Inspect Content-Type written by c.JSON (`application/json; charset=utf-8`).
  2. Compare against the negotiated response codec (driven by the request's Accept header + HERALD_DEFAULT_RESPONSE_CODEC env).
  3. If the client preferred TOON, JSON-decode the buffered body into `interface{}` and re-encode via MarshalChosen("application/toon").
  4. Rewrite the Content-Type header AND the body before flushing to the underlying ResponseWriter.

BindNegotiated is the symmetric request-side helper handlers can use instead of c.BindJSON when they need to honour an inbound Content-Type of application/toon.

§107 anti-bluff watershed (load-bearing):

  • The middleware NEVER writes JSON bytes under an `application/toon` Content-Type. The transcode step is the structural guarantee: either MarshalChosen succeeds and we emit real TOON bytes (the paired test asserts byte-0 is not `{`) OR we keep the original JSON body AND restore the `application/json` Content-Type header. There is no third path. The 2026-05-17 PASS-bluff (JSON bytes wearing an application/toon CT) is structurally impossible here.
  • The round-trip test (TestTOONMiddleware_RoundTrip_StructEqual) asserts reflect.DeepEqual against the original Receipt struct, not "no error". The size test (TestTOONMiddleware_RoundTrip_SizeSmaller) asserts TOON bytes are strictly smaller than JSON for the same struct — proving real codec benefit, not a header swap.

Constitutional anchors: Universal §11.4 (PASS-bluff prevention), Herald §107 (end-user-usability covenant), W4b plan Task 4.

Index

Constants

View Source
const (
	// MediaTypeTOON is the canonical Content-Type for TOON-encoded bodies.
	// Mirrors digital.vasic.toon/pkg/toon.ContentType (the smoke test in
	// toon_smoke_test.go pins value equality on the upstream constant).
	MediaTypeTOON = "application/toon"

	// MediaTypeJSON is the canonical Content-Type for JSON-encoded bodies.
	MediaTypeJSON = "application/json"
)

Media-type constants. Kept as exported package-level values so callers (Gin handlers, tests) can reference them without re-typing the strings.

View Source
const EnvDefaultResponseCodec = "HERALD_DEFAULT_RESPONSE_CODEC"

EnvDefaultResponseCodec is the operator-facing environment-variable name controlling the *default* response codec when the client's Accept header is `*/*` or absent. Values: "toon" (default), "json". Anything else silently falls back to "toon" — the Herald default per W4b operator decision 1.

View Source
const HeraldBrotliDefaultLevel = 6

HeraldBrotliDefaultLevel is the operator-locked default Brotli quality used by all Herald flavors when no operator override is supplied. 6 is one notch above the andybalholm/brotli library default (5); balanced CPU/ratio for Herald's small responses (256B-10KiB).

The full quality range per RFC 7932 is 0 (no compression — store only) through 11 (maximum compression). Higher = slower encode, lower = faster encode + larger output. Decode cost is roughly constant.

Variables

View Source
var (
	BuildVersion = "dev"
	BuildCommit  = "unknown"
	BuildTime    = "unknown"
)

Build-info variables. Set via -ldflags at build time; otherwise default to "dev" / unknown markers so a raw `go run` invocation is still operational. §107: an empty version field is a bluff — VersionCmd's JSON shape MUST populate every required key.

Functions

func AltSvcMiddleware

func AltSvcMiddleware(h3Port int) gin.HandlerFunc

AltSvcMiddleware returns a Gin handler that emits the Alt-Svc response header advertising HTTP/3 availability on the supplied UDP port. The emitted header is exactly:

Alt-Svc: h3=":<h3Port>"; ma=2592000

ma=2592000 is 30 days in seconds — RFC 7838 §3.1 recommends a long max-age so clients retain the upgrade hint across browser sessions.

Disabled mode: when h3Port <= 0 (HTTP/3 disabled via HERALD_DISABLE_HTTP3=1 or any configuration path that surfaces a non-positive port to serve.go), the middleware is a no-op and does NOT emit the header. Telling a client to upgrade to a port that isn't listening is a worse failure mode than letting them stay on HTTP/1.1+HTTP/2 — the client would burn UDP handshake attempts and silently fall back, polluting metrics.

The wrapped middleware is digital.vasic.middleware/pkg/altsvc.New bridged through digital.vasic.middleware/pkg/gin.Wrap so the Gin chain sees a native gin.HandlerFunc.

func BindNegotiated

func BindNegotiated(c *gin.Context, v any) error

BindNegotiated reads the request body and decodes it into v using the codec implied by the request's Content-Type header. Handlers use this instead of c.BindJSON when they need to accept either JSON or TOON request bodies.

Behaviour:

  • Content-Type: application/toon → TOON decode
  • Content-Type: application/json → JSON decode
  • Content-Type: empty / "*/*" → JSON decode (curl -d '...' compat per W4b operator decision 2)
  • Content-Type: application/xml etc. → JSON decode (best-effort fallback; UnmarshalChosen will surface a clear error if the bytes are not JSON)
  • empty body → returns nil, leaves v untouched (handler decides whether absence is OK)
  • read or decode error → returns a wrapped error naming the codec for the caller to map to 4xx

func BindServeFlags

func BindServeFlags(cmd *cobra.Command, port *int, tlsCert, tlsKey *string, noBrotli *bool)

BindServeFlags attaches the standard `serve` flag set (--http-port, --tls-cert, --tls-key, --no-brotli) to cmd, writing into the supplied destination variables. Exported so flavor binaries that compose a custom cobra command (pherald — needs lazy Runner construction before calling RunServe) get IDENTICAL flag UX to the shared ServeCmd path.

Per §107: divergent flag UX between flavors is a covenant violation — operators muscle-memorising --tls-cert on cherald and getting silent failure on pherald would be a paper-cut bluff. Centralising the flag definitions here keeps every binary lockstep.

func BrotliMiddleware

func BrotliMiddleware(quality int) gin.HandlerFunc

BrotliMiddleware returns a Gin handler that compresses response bodies with Brotli (quality `quality`) when the client's Accept-Encoding includes "br" AND the response body is at least 256 bytes AND the Content-Type is in the upstream's compressible-types list (application/json + text/* + the standard upstream defaults).

Quality resolution:

  • quality > 0: use that level directly (clamped to [0, 11] — out-of-range falls back to the operator-locked default).
  • quality == 0: read HERALD_BROTLI_LEVEL env var; parse + clamp; if env unset OR unparseable OR out-of-range, default to 6.

Per §107: the test brotli_test.go MUST decode the response body via github.com/andybalholm/brotli.NewReader and byte-compare it against the source. An implementation that sets Content-Encoding: br but writes identity bytes (a real failure mode if a middleware bug short-circuits the encoder) would pass a header-only assertion but fail decode-and-compare.

func HealthzHandler

func HealthzHandler(br commons.Branding) gin.HandlerFunc

HealthzHandler returns 200 with {status:"ok",flavor,build:{version,commit,go_version}}.

func MarshalChosen

func MarshalChosen(v any, contentType string) ([]byte, error)

MarshalChosen serialises v using the codec identified by contentType. Returns the encoded bytes or an error.

Supported content-types:

  • "application/toon" → digital.vasic.toon/pkg/toon.Marshal (the REAL upstream TOON wire-up via github.com/toon-format/toon-go; never JSON bytes wearing a TOON content-type — §107 anti-bluff).
  • "application/json" → encoding/json.Marshal.

Any other content-type returns an explicit error naming the unsupported CT. The error message includes the bad value so observability captures it without the caller having to log separately.

§107 anti-bluff: callers MUST verify that the returned bytes match the claimed content-type — the paired test asserts the TOON bytes do not begin with '{' or '[' (the original 2026-05-17 PASS-bluff signature).

func MetricsHandler

func MetricsHandler(br commons.Branding) gin.HandlerFunc

MetricsHandler returns Prometheus text with a <prefix>_build_info gauge. The gauge prefix follows the documented Prometheus convention for Herald flavors: BinaryName takes precedence (so pherald emits "pherald_build_info", sherald emits "sherald_build_info", …); when BinaryName is empty we fall back to Flavor (Wave 2 tests pass Flavor directly as the binary name). Wave 2 emits just the gauge; full Prometheus client wiring is a follow-up HRD (live observability per spec §17).

func NegotiateContentType

func NegotiateContentType(acceptHeader, contentTypeHeader string) (responseCT, requestCT string)

NegotiateContentType inspects an inbound HTTP request's Accept + Content-Type headers and decides:

  • responseCT: the Content-Type the server SHOULD set on the response body — determined by the Accept header per the policy ladder above.
  • requestCT: the Content-Type the server SHOULD use to decode the request body — determined by the Content-Type header (defaulting to JSON when absent or `*/*`).

The function is pure (no Gin/net.http coupling) and reads ONLY the env var HERALD_DEFAULT_RESPONSE_CODEC for its `*/*` / empty-Accept fallback decision (operator opt-out per W4b decision 3).

Header parsing follows RFC 7231 §5.3.2 (media-range + optional `q` parameter). Wildcards beyond `*/*` (e.g., `application/*`) are treated as `*/*` because Herald only ever serves application/{toon,json}.

The function NEVER returns an empty responseCT/requestCT — a caller is always given a concrete content-type it can encode/decode against. The safety-fallback when the client asks for something we can't speak (e.g., `Accept: application/xml`) is JSON, NOT a lie: we return "application/json" so the eventual response header matches the bytes we actually emit. The original 2026-05-17 PASS-bluff revision served JSON bytes under `Content-Type: application/toon`; that is structurally impossible at this layer because the codec dispatch in MarshalChosen keys on the SAME string returned here.

func NewRootCmd

func NewRootCmd(br commons.Branding, opts ...RootOpt) *cobra.Command

NewRootCmd returns the top-level Cobra command for a flavor. Branding drives Use / Short / Long / Version output. Use is the binary name (e.g. "pherald") since Cobra renders it as the invocation command in --help; Flavor (single-letter key, e.g. "p") and Prefix (3-letter, e.g. "PHR") appear in the long description for §8.2 context.

Falls back to br.Flavor if BinaryName is unset so callers that only populate Flavor (older tests / synthetic Branding) still produce a usable Cobra command.

func ReadyzHandler

func ReadyzHandler(br commons.Branding) gin.HandlerFunc

ReadyzHandler returns 200 with {status:"ready"}. Wave 2 doesn't probe real readiness — flavor implementations may extend later via custom Route entries that override this builtin.

func RunServe

func RunServe(ctx context.Context, opts ServeOpts, port int) error

RunServe is the exported dual-listener serve loop. It builds the Gin engine, registers healthz/readyz/metrics + opts.Middleware + opts.Routes, resolves the TLS cert source, starts the TCP/H2 listener (and the UDP/H3 listener when not opts.DisableH3), and blocks until SIGINT/SIGTERM/ctx-cancel. On terminal signal it gracefully shuts down BOTH listeners in parallel (5s grace each).

Callable two ways:

  1. Via ServeCmd's RunE — the common case for flavors with eager dependency construction (cherald, sherald, bherald, …).
  2. Directly from a flavor's custom cobra command — for flavors that need lazy dependency construction (pherald builds its Runner and verifier inside its own RunE so `pherald version` / `pherald migrate` don't require PG + JWT env vars). The flavor binds the standard flags via BindServeFlags, builds opts.Routes + opts.Middleware lazily, then calls RunServe(ctx, opts, port).

The two paths produce IDENTICAL listener behavior (dual TCP+UDP when not disabled, single TCP when DisableH3). Env-driven auto-detection (HERALD_DISABLE_HTTP3=1, HERALD_AUTH_MODE=jwks) lives here so both callers get it for free.

Pass port = 0 to use opts.Branding.DefaultPort.

func ServeCmd

func ServeCmd(opts ServeOpts) *cobra.Command

ServeCmd is the `<flavor>herald serve` subcommand for serving flavors. Binds a Gin engine with healthz/readyz/metrics + every Route in opts.Routes (nil-Handler+non-empty-HRD routes get StubRouteHandler).

Listener model (Wave 4a Task 6):

  • DisableH3 == false (default): binds BOTH a TCP/HTTPS+H2 listener AND a UDP/H3 listener on --http-port. Both serve the same Gin engine. Auto-injects AltSvcMiddleware + BrotliMiddleware + TOONMiddleware (Wave 4b T6) into the chain so TCP clients see Alt-Svc upgrade hints + Accept-Encoding: br responses get compressed + Accept: application/toon responses get TOON-encoded. Requires a TLS cert (operator flag, env, or dev-autogen depending on ProdMode).
  • DisableH3 == true: binds ONLY the TCP listener (HERALD_DISABLE_HTTP3=1 escape hatch). If a cert is supplied, the listener runs TLS; if not, it falls back to plaintext HTTP/1.1+H2c, matching pre-T6 behavior for dev workflows that don't want TLS.

Graceful shutdown on SIGTERM/SIGINT/ctx-cancel: BOTH listeners get up to 5s grace each. Returns nil on clean shutdown, error on bind failure.

Per §107: a "serve started" PASS without observing a healthz round- trip on BOTH listeners is a bluff. The integration tests start the server and verify both listeners actually accept traffic before declaring success; post-shutdown, the TCP port returns connection- refused and the UDP port is re-bindable by an unrelated listener (proof the QUIC stack released the socket).

Wave 4a Task 7.5 (pherald unification): the dual-listener body is now in RunServe(ctx, opts). ServeCmd is just flag-binding + a thin RunE shim. Flavor binaries that need lazy dependency construction (e.g. pherald, which builds its Runner against PG only at serve-time) can call BindServeFlags + RunServe directly, bypassing the cobra wrapper while still getting the exact same dual-listener engine.

func StubCmd

func StubCmd(name, hrd, description string) *cobra.Command

StubCmd returns a Cobra subcommand that always fails with a 501-style error citing the HRD that will implement it. Used by every flavor's internal/stubs/ to register §43 commands not yet implemented.

Per Herald §107: the error message MUST contain the HRD pointer + the command name + the literal "not yet implemented" — these are the three substrings the e2e_bluff_hunt E31 invariant asserts on.

func StubRouteHandler

func StubRouteHandler(route Route) gin.HandlerFunc

StubRouteHandler returns 501 with a JSON body citing the HRD that will implement the route. Used for Wave 2 placeholder routes.

func TOONMiddleware

func TOONMiddleware() gin.HandlerFunc

TOONMiddleware returns a Gin handler that intercepts response bodies emitted via c.JSON (or c.Render with render.JSON) and transcodes them to TOON when the client's Accept header (or the Herald-default codec env) prefers application/toon.

Flow:

  1. Negotiate response codec from Accept + HERALD_DEFAULT_RESPONSE_CODEC.
  2. Wrap c.Writer in toonResponseWriter; handlers see no difference — they still call c.JSON(status, obj).
  3. After c.Next(), inspect the buffered body's Content-Type: - If the writer recorded application/json AND the negotiated codec is application/toon, JSON-decode the body into a generic interface{}, then MarshalChosen → TOON bytes. - Otherwise, pass through unchanged.
  4. Flush to the underlying ResponseWriter with the correct Content-Type + status + bytes.

Why this design over a Respond(c, status, obj) helper?

  • It is a drop-in: every existing handler that calls c.JSON keeps working. The W4b plan's original sketch (Respond helper) requires editing every handler; the middleware approach narrows the blast radius of W4b T5/T6 to "add the middleware to the chain".
  • The transcode is bounded by JSON round-trip semantics (numbers become float64, etc.) — for Herald's payloads (Receipts, Compliance decisions, Safety states) those round-trip losslessly. If a flavor ever ships a payload that doesn't round-trip cleanly, the per- handler escape hatch is to call MarshalChosen directly + c.Data.

Non-goals (deliberate):

  • This middleware does NOT compress (Brotli is a separate layer).
  • It does NOT touch responses that were never set as JSON (e.g., MetricsHandler emits text/plain; the middleware passes those through unchanged).
  • It does NOT short-circuit when negotiation returns JSON — the pass-through path is identical, just no transcode.

func UnmarshalChosen

func UnmarshalChosen(data []byte, v any, contentType string) error

UnmarshalChosen decodes data into v using the codec identified by contentType. v MUST be a non-nil pointer.

Supported content-types: same as MarshalChosen — application/toon (digital.vasic.toon/pkg/toon.Unmarshal) and application/json (encoding/json.Unmarshal).

Any other content-type returns an explicit error naming the unsupported CT (per the §107 anti-bluff rule: never silently fall back to JSON when the caller named a CT we can't handle — that would mask a misrouted body).

func VersionCmd

func VersionCmd(br commons.Branding) *cobra.Command

VersionCmd is the `<flavor>herald version` subcommand. Prints human- readable build info by default; `--json` returns the canonical JSON shape used by e2e_bluff_hunt E2/E19-E24 (required fields: binary, flavor, version, go_version, os, arch — none may be empty).

Types

type RootOpt

type RootOpt func(*cobra.Command)

RootOpt is a functional option for NewRootCmd.

type Route

type Route struct {
	Method      string          // "GET", "POST", ...
	Path        string          // "/v1/compliance"
	Handler     gin.HandlerFunc // if nil + HRD non-empty, StubRouteHandler is used
	Description string          // operator-readable summary
	HRD         string          // "HRD-028" for 501 stubs; "" for live routes
}

Route declares one HTTP route. Wave 2 stubs return 501 with the HRD pointer body when Handler is nil + HRD is non-empty.

type ServeOpts

type ServeOpts struct {
	Branding      commons.Branding
	Routes        []Route
	Middleware    []gin.HandlerFunc
	TLSCertPath   string
	TLSKeyPath    string
	ProdMode      bool
	DisableH3     bool
	DisableBrotli bool
}

ServeOpts configures ServeCmd.

Required:

Branding   — drives the default port + healthz/metrics gauge name.

Optional flavor-specific:

Routes     — flavor-specific routes appended after healthz/readyz/metrics.
Middleware — flavor-specific Gin middleware chain registered immediately
             after gin.Recovery() + the auto-wired Brotli/Alt-Svc layers
             + before any handler. Typical use is request-ID
             propagation, tenant extraction, OTel tracing.

Wave 4a TLS / HTTP/3 controls (zero-value defaults preserve pre-T6 behavior so existing flavor binaries are not broken by the dual- listener refactor):

TLSCertPath    — operator-supplied --tls-cert path (forwarded to
                 commons_tls.ResolveCertSource).
TLSKeyPath     — operator-supplied --tls-key path.
ProdMode       — true ⇒ ResolveCertSource refuses dev-autogen. The
                 caller's convention: set this to
                 (os.Getenv("HERALD_AUTH_MODE") == "jwks").
DisableH3      — true ⇒ HTTP/3 listener is NOT started, the Alt-Svc
                 middleware becomes a no-op, and the TCP listener
                 falls back to plaintext (or TLS if a cert was
                 supplied explicitly). Driven by
                 HERALD_DISABLE_HTTP3=1 in the caller.
DisableBrotli  — true ⇒ Brotli middleware is NOT auto-wired. Useful
                 for routes that stream + cannot be buffered.

Jump to

Keyboard shortcuts

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