middleware

package
v1.46.31 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Package middleware provides HTTP middleware for the Commerce API.

This file implements Cloudflare-aware HTTP cache control middleware. Routes served via api.hanzo.ai sit behind CF; correct Cache-Control headers are the only lever we have to control what CF caches.

Strategy:

  • All authenticated routes: Cache-Control: private, no-store (CF must not cache these — they carry per-user data)
  • Public read-only routes (billing plans, product catalog): Cache-Control: public with a TTL appropriate to how often the data changes.
  • All mutation routes (POST/PUT/PATCH/DELETE): Cache-Control: no-store regardless of the route's other classification.

CF Cache-Tag headers allow targeted cache purging when data changes. Add tags in individual handlers via SetCFCacheTags(c, "plans", "org:xyz").

Index

Constants

View Source
const HeaderMethodOverride = "X-HTTP-Method-Override"

HeaderMethodOverride is a commonly used Http header to override the method.

View Source
const ParamMethodOverride = "_method"

ParamMethodOverride is a commonly used HTML form parameter to override the method.

Variables

View Source
var AppEngine = RequestContext

AppEngine is a legacy alias for RequestContext. Deprecated: use RequestContext instead.

View Source
var ErrInvalidOverrideMethod = errors.New("invalid override method")

ErrInvalidOverrideMethod is returned when an invalid http method was given to OverrideRequestMethod.

View Source
var GetAppEngine = GetContext

GetAppEngine is a legacy alias for GetContext. Deprecated: use GetContext instead.

View Source
var HttpMethods = []string{"PUT", "PATCH", "DELETE"}

Functions

func AccessControl

func AccessControl(allowOrigin string) gin.HandlerFunc

func AccountRequired

func AccountRequired() gin.HandlerFunc

func AcquireOrganization

func AcquireOrganization(moduleName string) gin.HandlerFunc

func AcquireUser

func AcquireUser(moduleName string) gin.HandlerFunc

func AddHost

func AddHost() gin.HandlerFunc

Automatically get the Host header so we can decide what to do with a given request.

func BasicAuth

func BasicAuth() gin.HandlerFunc

func CFCacheTags added in v1.36.4

func CFCacheTags(tags ...string) gin.HandlerFunc

CFCacheTags returns middleware that sets Cache-Tag header(s). Use on route groups whose entries should be purgeable as a unit.

func CacheNoStore added in v1.36.4

func CacheNoStore() gin.HandlerFunc

CacheNoStore disables all caching unconditionally. Use on auth flows, checkout, and payment callbacks.

func CachePrivate added in v1.36.4

func CachePrivate() gin.HandlerFunc

CachePrivate sets Cache-Control: private, no-store. Use on all authenticated per-user or per-org routes. CF will not cache these responses.

func CachePublic added in v1.36.4

func CachePublic(ttl int) gin.HandlerFunc

CachePublic returns middleware that sets public cache headers with the given TTL.

CF caches for ttl seconds (s-maxage). Browsers cache for ttl/2 seconds to ensure fresh content at browser re-visits. stale-while-revalidate allows CF to serve stale content while fetching fresh in background.

Mutations (POST/PUT/PATCH/DELETE) are always no-store regardless.

func CachePublicTTL added in v1.36.4

func CachePublicTTL(ttl time.Duration) gin.HandlerFunc

CachePublicTTL is CachePublic accepting a time.Duration.

func CheckLogin

func CheckLogin() gin.HandlerFunc

Updates session with login information, does not require it

func DetectOverrides

func DetectOverrides() gin.HandlerFunc

Check query for special config override params and update session.

func DetectTest

func DetectTest(query *url.Values) bool

func DetectVerbose

func DetectVerbose(query *url.Values) bool

Try and detect verbose flag set on request, we only log DEBUG level in production if verbose=1 is added as a query param.

func EdgeAuth added in v1.42.38

func EdgeAuth() gin.HandlerFunc

EdgeAuth is the standalone-edge trust boundary for a directly-exposed commerce-api. It is a NO-OP unless COMMERCE_EDGE_AUTH=true, so gateway-fronted deployments (where hanzoai/gateway already strips and mints identity) are untouched — one trust boundary at a time.

When enabled, on every request it:

  1. Strips client-supplied identity headers (anti-spoofing). Without this, `curl -H "X-Org-Id: <org>"` reads any org's billing because downstream trusts the header (gateway is supposed to have stripped it — but commerce-api is not behind the gateway).

  2. If a Bearer IAM JWT is present, verifies it against the IAM JWKS (fail-closed, RSA-pinned) and mints X-Org-Id / X-User-Id / X-User-Email / X-User-IsAdmin / X-User-Permissions from the validated claims, so the existing IAMTokenRequired + handlers resolve the caller's org exactly as in the gateway path.

  3. For /billing/ requests, locks the billing-subject to the caller's own org slug — in the query params (user / userId / customerId) for reads, AND in the JSON body of writes (POST/PUT/PATCH) — so a browser can only ever read or WRITE its OWN org's billing (per-org isolation) regardless of what it puts on the URL or in the body. Without the body half, a write whose body customerId differs from the locked query subject lands under an arbitrary key that every read (forced to the slug) can never see — silently orphaning the record.

Service tokens (COMMERCE_SERVICE_TOKEN) and hk-/sk- API keys are not JWTs, so step 2 skips them. Their client-supplied X-Org-Id is NOT restored to the trusted header (that would let IAMTokenRequired treat an unvalidated token as a verified identity — the bypass this boundary now closes); it is stashed in a PRIVATE context key that ONLY TokenRequired's service-token branch reads, after it has verified the bearer equals COMMERCE_SERVICE_TOKEN.

ORDER: EdgeAuth MUST run BEFORE pkg/auth.Gin (both installed by Bootstrap via server.go installIdentityBoundary, ahead of every route group). auth.Gin binds the X-Org-Id header into the request CONTEXT; if EdgeAuth ran after it, stripping the header would leave the spoofed value in the context (which IAMTokenRequired reads first). Mounting EdgeAuth first means auth.Gin only ever sees the stripped/minted headers.

The IAM client is resolved lazily (iammiddleware.Client()) so mount order is independent of when iammiddleware.Init() runs at boot.

func ErrorHTML

func ErrorHTML(c *gin.Context, stack string, err error)

Display errors in HTML

func ErrorHTMLDev

func ErrorHTMLDev(c *gin.Context, stack string, err error)

func ErrorHandler

func ErrorHandler() gin.HandlerFunc

Error middleware

func ErrorHandlerJSON

func ErrorHandlerJSON() gin.HandlerFunc

func ErrorJSON

func ErrorJSON(c *gin.Context, stack string, err error)

Display errors in JSON

func ErrorJSONDev

func ErrorJSONDev(c *gin.Context, stack string, err error)

func ErrorLogger

func ErrorLogger() gin.HandlerFunc

func ErrorLoggerT

func ErrorLoggerT(typ gin.ErrorType) gin.HandlerFunc

func GetAccessToken

func GetAccessToken(c *gin.Context) string

func GetContext added in v1.35.2

func GetContext(c *gin.Context) context.Context

GetContext retrieves the request context from the Gin context.

func GetCurrentUser

func GetCurrentUser(c *gin.Context) *user.User

func GetNamespace

func GetNamespace(c *gin.Context) context.Context

func GetOrganization

func GetOrganization(c *gin.Context) *organization.Organization

func GetOrganizationOK added in v1.42.47

func GetOrganizationOK(c *gin.Context) (*organization.Organization, bool)

GetOrganizationOK returns the request organization without panicking when it is absent. Use this on handlers mounted outside the auth-token group (e.g. signature-verified webhook ingress) where no session has set an organization; GetOrganization would MustGet-panic there.

func GetPermissions

func GetPermissions(c *gin.Context) bit.Field

func GetToken

func GetToken(c *gin.Context) *accesstoken.AccessToken

func GetUser

func GetUser(c *gin.Context) *user.User

func IsServiceToken added in v1.46.31

func IsServiceToken(c *gin.Context) bool

IsServiceToken reports whether TokenRequired verified this request's bearer against COMMERCE_SERVICE_TOKEN. Fail-closed: a missing or non-bool value → false. It is a POSITIVE, source-recorded fact (set where the secret is checked), never re-derived from indirect signals like the Admin bit or absence of IAM identity — a legacy org access token is also "not IAM authenticated" yet must NOT pass the money-mint gate.

func IsValidMethodOverride

func IsValidMethodOverride(method string) bool

func LiveReload

func LiveReload() gin.HandlerFunc

func Log

func Log(c *gin.Context)

func Logger

func Logger() gin.HandlerFunc

func LoginRequired

func LoginRequired(moduleName string) gin.HandlerFunc

Require login to view route

func LogoutRequired

func LogoutRequired(moduleName string) gin.HandlerFunc

Required to be logged out to view

func MethodOverride

func MethodOverride() gin.HandlerFunc

func Namespace

func Namespace() gin.HandlerFunc

Namespace applies the organization's namespace to the request context.

func NotFoundHandler

func NotFoundHandler() gin.HandlerFunc

Serve custom 404 page.

func OverrideRequestMethod

func OverrideRequestMethod(c *gin.Context, method string) error

OverrideRequestMethod overrides the http request's method with the specified method.

func ParseToken

func ParseToken(c *gin.Context)

func PlatformOnly added in v1.46.31

func PlatformOnly() gin.HandlerFunc

PlatformOnly restricts a route to the ONLY two principals allowed to MINT money / spendable balance:

  1. the internal service (cloud-api → commerce), authenticated by a bearer equal to COMMERCE_SERVICE_TOKEN — recorded by TokenRequired's service-token branch as IsServiceToken(c); and
  2. a Hanzo PLATFORM (global) administrator — auth.IAMClaims.GlobalAdmin(): the spoof-proof isGlobalAdmin claim (gateway/EdgeAuth X-User-IsGlobalAdmin) OR membership in the "admin" org.

It deliberately does NOT admit the org-level Admin bit (permission.Admin). An org OWNER carries org-level IsAdmin=true within their own org (IAM), which the gateway/EdgeAuth mints into X-User-Permissions = Admin|Live; a legacy per-org access token can hold Admin too. Gating the money-mint billing routes on that bit — via TokenRequired(permission.Admin) alone — let ANY org owner self-credit unlimited balance (POST /v1/billing/deposit &c.) → unlimited free inference. That is the real-money-GA blocker this gate closes. It is the same org-admin-vs-global-admin anti-conflation the codebase enforces for cross-org actions (checkout tenant admin, the edge billing ?org override).

MOUNT IT AFTER TokenRequired(permission.Admin): TokenRequired resolves the org (service-token + legacy paths), sets c["permissions"], and stamps the service-token marker; PlatformOnly then NARROWS who may proceed to the handler. It never widens access — a caller already rejected by TokenRequired (401) never reaches here.

Fail-closed: neither signal present → 403, handler not reached.

func RequestContext added in v1.35.2

func RequestContext() gin.HandlerFunc

RequestContext extracts the standard Go context from the HTTP request and stores it in the Gin context for downstream handlers.

func RequireAdmin added in v1.42.51

func RequireAdmin(c *gin.Context) bool

RequireAdmin is the ONE admin gate the money-moving handlers use — IAM-aware AND legacy/service-token-aware. It fails closed (403) unless the caller is an admin, and is enforced INSIDE each money handler because the route-level TokenRequired(permission.Admin) middleware is a NO-OP on the IAM path: it short-circuits (c.Next) for any IAM-authenticated request WITHOUT checking the Admin bit (Red HIGH-4). A handler must never trust that gate on its own.

Precedence (fail-closed):

  1. Permissions bit — the legacy access token AND the service token both set c["permissions"] with permission.Admin when the caller is admin (middleware/accesstoken.go). Honored FIRST so the trusted M2M service-token money path (cloud-api → commerce, which carries X-Org-Id) is authorized by its verified token, not mistaken for a spoofable IAM-edge header identity.
  2. IAM identity — the gateway/EdgeAuth-minted, JWT-verified claims must carry org-level IsAdmin OR platform GlobalAdmin.

These are per-ORG money actions (the caller acts within its own resolved namespace), so org-level admin suffices and a global admin is also allowed (superset). Cross-tenant/platform-global actions gate on the STRICTER GlobalAdmin predicate instead (api/catalog.requireGlobalAdmin, checkout.isSuperadmin), never this one.

Returns true when admin; writes a 403 and returns false otherwise. Reads c["permissions"] without MustGet so a handler mounted without the token gate fails closed (403) rather than panicking (500).

func SetCFCacheTags added in v1.36.4

func SetCFCacheTags(c *gin.Context, tags ...string)

SetCFCacheTags adds Cloudflare Cache-Tag header values to the response. Tags are used for targeted cache purging (e.g. purge all "plans" entries). Multiple calls accumulate; tags are comma-joined as CF requires.

Example: SetCFCacheTags(c, "plans", "org:hanzo")

func Static

func Static(urlRoot string) gin.HandlerFunc

func TokenPermits

func TokenPermits(masks ...bit.Mask) gin.HandlerFunc

Permissions required to access route

func TokenRequired

func TokenRequired(masks ...bit.Mask) gin.HandlerFunc

Parses token, default permissions check

func UnavailableHandler

func UnavailableHandler() gin.HandlerFunc

Serve custom 503 page.

Types

type ErrorDisplayer

type ErrorDisplayer func(c *gin.Context, message string, err error)

Directories

Path Synopsis
Package iammiddleware is the gateway-trust shim for legacy call sites.
Package iammiddleware is the gateway-trust shim for legacy call sites.

Jump to

Keyboard shortcuts

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