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
- Variables
- func AccessControl(allowOrigin string) gin.HandlerFunc
- func AccountRequired() gin.HandlerFunc
- func AcquireOrganization(moduleName string) gin.HandlerFunc
- func AcquireUser(moduleName string) gin.HandlerFunc
- func AddHost() gin.HandlerFunc
- func AuthorizeMint(c *gin.Context)
- func BasicAuth() gin.HandlerFunc
- func CFCacheTags(tags ...string) gin.HandlerFunc
- func CacheNoStore() gin.HandlerFunc
- func CachePrivate() gin.HandlerFunc
- func CachePublic(ttl int) gin.HandlerFunc
- func CachePublicTTL(ttl time.Duration) gin.HandlerFunc
- func CheckLogin() gin.HandlerFunc
- func DetectOverrides() gin.HandlerFunc
- func DetectTest(query *url.Values) bool
- func DetectVerbose(query *url.Values) bool
- func EdgeAuth() gin.HandlerFunc
- func ErrorHTML(c *gin.Context, stack string, err error)
- func ErrorHTMLDev(c *gin.Context, stack string, err error)
- func ErrorHandler() gin.HandlerFunc
- func ErrorHandlerJSON() gin.HandlerFunc
- func ErrorJSON(c *gin.Context, stack string, err error)
- func ErrorJSONDev(c *gin.Context, stack string, err error)
- func ErrorLogger() gin.HandlerFunc
- func ErrorLoggerT(typ gin.ErrorType) gin.HandlerFunc
- func GetAccessToken(c *gin.Context) string
- func GetContext(c *gin.Context) context.Context
- func GetCurrentUser(c *gin.Context) *user.User
- func GetNamespace(c *gin.Context) context.Context
- func GetOrganization(c *gin.Context) *organization.Organization
- func GetOrganizationOK(c *gin.Context) (*organization.Organization, bool)
- func GetPermissions(c *gin.Context) bit.Field
- func GetToken(c *gin.Context) *accesstoken.AccessToken
- func GetUser(c *gin.Context) *user.User
- func IsServiceToken(c *gin.Context) bool
- func IsValidMethodOverride(method string) bool
- func LiveReload() gin.HandlerFunc
- func Log(c *gin.Context)
- func Logger() gin.HandlerFunc
- func LoginRequired(moduleName string) gin.HandlerFunc
- func LogoutRequired(moduleName string) gin.HandlerFunc
- func MayMintMoney(c *gin.Context) bool
- func MethodOverride() gin.HandlerFunc
- func Namespace() gin.HandlerFunc
- func NotFoundHandler() gin.HandlerFunc
- func OverrideRequestMethod(c *gin.Context, method string) error
- func ParseToken(c *gin.Context)
- func PlatformOnly() gin.HandlerFunc
- func RequestContext() gin.HandlerFunc
- func RequireAdmin(c *gin.Context) bool
- func SetCFCacheTags(c *gin.Context, tags ...string)
- func Static(urlRoot string) gin.HandlerFunc
- func TokenPermits(masks ...bit.Mask) gin.HandlerFunc
- func TokenRequired(masks ...bit.Mask) gin.HandlerFunc
- func UnavailableHandler() gin.HandlerFunc
- type ErrorDisplayer
Constants ¶
const HeaderMethodOverride = "X-HTTP-Method-Override"
HeaderMethodOverride is a commonly used Http header to override the method.
const ParamMethodOverride = "_method"
ParamMethodOverride is a commonly used HTML form parameter to override the method.
Variables ¶
var AppEngine = RequestContext
AppEngine is a legacy alias for RequestContext. Deprecated: use RequestContext instead.
var ErrInvalidOverrideMethod = errors.New("invalid override method")
ErrInvalidOverrideMethod is returned when an invalid http method was given to OverrideRequestMethod.
var GetAppEngine = GetContext
GetAppEngine is a legacy alias for GetContext. Deprecated: use GetContext instead.
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 ¶
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 DetectVerbose ¶
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:
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).
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.
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 ErrorHandlerJSON ¶
func ErrorHandlerJSON() gin.HandlerFunc
func ErrorLogger ¶
func ErrorLogger() gin.HandlerFunc
func ErrorLoggerT ¶
func ErrorLoggerT(typ gin.ErrorType) gin.HandlerFunc
func GetAccessToken ¶
func GetContext ¶
GetContext retrieves the request context from the Gin 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 GetToken ¶
func GetToken(c *gin.Context) *accesstoken.AccessToken
func IsServiceToken ¶
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 LiveReload ¶
func LiveReload() gin.HandlerFunc
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 ¶
MayMintMoney is THE single predicate for "may this caller MINT money / spendable balance". It admits exactly the two principals PlatformOnly admits:
- the verified internal service token (cloud-api → commerce), IsServiceToken(c); and
- 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 OverrideRequestMethod ¶
OverrideRequestMethod overrides the http request's method with the specified method.
func ParseToken ¶
func PlatformOnly ¶
func PlatformOnly() gin.HandlerFunc
PlatformOnly restricts a route to the ONLY two principals allowed to MINT money / spendable balance:
- 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
- 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 ¶
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):
- 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.
- 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 ¶
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
Types ¶
Source Files
¶
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). |