middleware

package
v1.799.2 Latest Latest
Warning

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

Go to latest
Published: Jul 12, 2026 License: Apache-2.0, MIT Imports: 33 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 AuthorizeMint

func AuthorizeMint(c *gin.Context)

AuthorizeMint stamps this request's datastore context as authorized to mint spendable balance, so a downstream ledger write (through an Organization.Namespaced-derived datastore) passes mintauth.Enforce. It is the ONE way an HTTP handler grants the mint capability the ledger sink demands — call it AFTER establishing a proven mint authority (MayMintMoney, a settled payment, or a server-fixed grant). It re-stores the gin "context" key, which Organization.Namespaced reads, so any datastore the handler builds afterward is authorized. Writes that build their datastore BEFORE the authority is known (top-up after the charge, webhook after signature) instead authorize the specific write via mintauth.WithAuthorized on that write's context.

func BasicAuth

func BasicAuth() gin.HandlerFunc

func CFCacheTags

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

func CacheNoStore() gin.HandlerFunc

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

func CachePrivate

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

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

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

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

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

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

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 MayMintMoney

func MayMintMoney(c *gin.Context) bool

MayMintMoney is THE single predicate for "may this caller MINT money / spendable balance". It admits exactly the two principals PlatformOnly admits:

  1. the verified internal service token (cloud-api → commerce), IsServiceToken(c); and
  2. a Hanzo PLATFORM SuperAdmin, auth.IAMClaims.IsSuperAdmin() — the spoof-proof isSuperAdmin claim OR membership in the "admin" org.

It deliberately does NOT admit the org-level Admin bit (an org OWNER's IAM isAdmin, or a legacy per-org access token). Use it wherever a mint decision is made OUTSIDE the route-middleware chain — the ZAP-over-HTTP dispatcher gates its money-mint method (billing.deposit) with it, and the allotment grant clamps a client plan override on it — so there is ONE expression of the mint principal, shared by the route gate (PlatformOnly) and every in-handler gate. Fail-closed: neither signal present → false.

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

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 SuperAdmin — auth.IAMClaims.IsSuperAdmin(): the spoof-proof isSuperAdmin claim (gateway/EdgeAuth X-User-IsSuperAdmin) 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-SuperAdmin 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

func RequestContext() gin.HandlerFunc

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

It also marks the stored context mint-gated (mintauth.WithGate): every inbound request is a potential untrusted principal, so any spendable-balance mint that flows from it must carry mint authorization or the ledger sink refuses it. This backs up the primary gate in Organization.Namespaced for the rare handler that builds a datastore from middleware.GetContext(c) directly. Authorization (PlatformOnly / settled payment / server-fixed grant) rides on top.

func RequireAdmin

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 SuperAdmin.

These are per-ORG money actions (the caller acts within its own resolved namespace), so org-level admin suffices and a SuperAdmin is also allowed (superset). Cross-tenant/platform-global actions gate on the STRICTER SuperAdmin predicate instead (api/catalog.requireSuperAdmin, 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

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.
Package svcorg resolves — and memoizes — the organization a verified service token acts on behalf of (cloud-api → commerce per-org billing).
Package svcorg resolves — and memoizes — the organization a verified service token acts on behalf of (cloud-api → commerce per-org billing).

Jump to

Keyboard shortcuts

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