middleware

package
v1.49.62 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 39 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 ErrInvalidOverrideMethod = errors.New("invalid override method")

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

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

Functions

func AccessControl

func AccessControl(allowOrigin string) zip.Handler

func AccountRequired

func AccountRequired() zip.Handler

func AcquireOrganization

func AcquireOrganization(moduleName string) zip.Handler

func AcquireUser

func AcquireUser(moduleName string) zip.Handler

func AddHost

func AddHost() zip.Handler

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

func AuthorizeMint added in v1.46.38

func AuthorizeMint(c *zip.Ctx)

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 "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() zip.Handler

func CFCacheTags added in v1.36.4

func CFCacheTags(tags ...string) zip.Handler

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() zip.Handler

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

func CachePrivate added in v1.36.4

func CachePrivate() zip.Handler

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) zip.Handler

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) zip.Handler

CachePublicTTL is CachePublic accepting a time.Duration.

func CheckLogin

func CheckLogin() zip.Handler

Updates session with login information, does not require it

func DetectOverrides

func DetectOverrides() zip.Handler

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() zip.Handler

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 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.Zip (both installed by Bootstrap via server.go installIdentityBoundary, ahead of every route group). pkg/auth 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 the identity binding 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 *zip.Ctx, stack string, err error)

Display errors in HTML

func ErrorHTMLDev

func ErrorHTMLDev(c *zip.Ctx, stack string, err error)

func ErrorHandler

func ErrorHandler() zip.Handler

Error middleware

func ErrorHandlerJSON

func ErrorHandlerJSON() zip.Handler

func ErrorJSON

func ErrorJSON(c *zip.Ctx, stack string, err error)

Display errors in JSON

func ErrorJSONDev

func ErrorJSONDev(c *zip.Ctx, stack string, err error)

func GetAccessToken

func GetAccessToken(c *zip.Ctx) string

func GetCurrentUser

func GetCurrentUser(c *zip.Ctx) *user.User

func GetNamespace

func GetNamespace(c *zip.Ctx) context.Context

func GetOrganization

func GetOrganization(c *zip.Ctx) *organization.Organization

func GetOrganizationOK added in v1.42.47

func GetOrganizationOK(c *zip.Ctx) (*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 panic there.

func GetPermissions

func GetPermissions(c *zip.Ctx) bit.Field

func GetToken

func GetToken(c *zip.Ctx) *accesstoken.AccessToken

func GetUser

func GetUser(c *zip.Ctx) *user.User

func IsPlatform added in v1.49.27

func IsPlatform(c *zip.Ctx) bool

IsPlatform is THE definition of the PLATFORM PRINCIPAL — the two callers that act for the platform itself rather than for a tenant:

  1. the verified internal service token (cloud-api → commerce, a scheduled job), IsServiceToken(c); and
  2. a Hanzo PLATFORM SuperAdmin, auth.IAMClaims.IsSuperAdmin() — the reserved "admin" org membership (owner=="admin").

It deliberately does NOT admit the org-level Admin bit (an org OWNER's IAM isAdmin, or a legacy per-org access token).

It answers WHO IS CALLING and nothing else. Asking grants no capability — the capability gates are built ON it (MayMintMoney for money, the catalog sync for upstream cost), so who-may-act is defined once and each capability names itself. Fail-closed: neither signal present → false.

func IsServiceToken added in v1.46.31

func IsServiceToken(c *zip.Ctx) 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() zip.Handler

func Log

func Log(c *zip.Ctx) error

func Logger

func Logger() zip.Handler

func LoginRequired

func LoginRequired(moduleName string) zip.Handler

Require login to view route

func LogoutRequired

func LogoutRequired(moduleName string) zip.Handler

Required to be logged out to view

func MayMintMoney added in v1.46.33

func MayMintMoney(c *zip.Ctx) bool

MayMintMoney is THE single predicate for "may this caller MINT money / spendable balance": the platform principal, and only it. 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.

func MayReadPlatform added in v1.46.44

func MayReadPlatform(c *zip.Ctx) bool

MayReadPlatform is THE single predicate for "may this caller READ cross-org PLATFORM (god-view) data" — the business metrics, vendor costs, and any other aggregate that spans every tenant. It admits exactly the principals a platform god-view trusts, and fails closed for everyone else:

  1. a Hanzo PLATFORM SuperAdmin — auth.IAMClaims.IsSuperAdmin(): owner=="admin" (the reserved admin org), from the gateway/EdgeAuth X-User-Owner header OR membership in the built-in "admin" org; and
  2. the trusted internal service token — IsServiceToken(c), the console's OWN global-admin-gated proxy forwarding (console → commerce) with the COMMERCE_SERVICE_TOKEN and X-Org-Id but NO user identity; and
  3. the same M2M forward observed structurally: the Admin permission bit is present AND there is NO IAM user identity (empty Subject). An IAM user ALWAYS carries a Subject (the JWT sub the gateway mints alongside any permission), so "Admin bit AND no IAM Subject" uniquely identifies the verified service token and can never be an org admin. This branch is the one requireCostsAdmin has always used; keeping it here means costs and the SaaS metrics god-view share ONE gate.

It deliberately does NOT admit the org-level Admin bit (permission.Admin) held by an org OWNER: the gateway mints permission.Admin from a tenant's own IsAdmin (edgeauth.permsHeader), so gating cross-org reads on that bit would let ANY org owner read the whole fleet's revenue. Only a SuperAdmin (or the trusted service token) may. This is the same org-admin-vs-global-admin anti-conflation PlatformOnly/MayMintMoney enforce for the money-MINT side.

Reads c["permissions"] without MustGet so a handler mounted without the token gate fails closed (false) rather than panicking. Fail-closed: none present → false.

func MethodOverride

func MethodOverride() zip.Handler

func Mint added in v1.49.0

func Mint(r zip.Router, prefix string) zip.Router

Mint returns a view of r on which EVERY registered route is a money-mint route: it prepends PlatformOnly() to the route's handler chain AND records the route in the mint registry (MintRoutes). Registration is the single declaration — the gate and the registry entry are the same act, so they cannot drift:

mint := middleware.Mint(api)
mint.Post("/deposit", Deposit)   // gated AND recorded

This replaces the CONVENTION `api.Post("/deposit", mintRequired, Deposit)`, where "this is a mint route" was knowable only by reading the middleware chain and a downstream consumer (cloud's billing-bridge allowlist) had to hand-copy the list. The gate's BEHAVIOR is unchanged: PlatformOnly is prepended, so it runs FIRST and the handler LAST, exactly as the explicit chain did.

The natural shape for an authz class is a group carrying its own Use (`api.Use(adminRequired)` one level up). Mint deliberately does NOT do that. It could not, when the router matched Use by PATH PREFIX rather than by group membership: `api.Group("").Use(PlatformOnly())` then gated every neighbouring route under /v1/billing, the org-admin reads included. zip v1.23 closed that — TestGroupUseIsMembershipScopedNotPrefix now holds the guarantee — so the gated-sub-group shape has become available. Mint still prepends the gate to each route's own chain, which gates exactly the declared routes and does not depend on which framework version is pinned. These are the routes that move money; taking the simpler shape is a change on its own evidence.

prefix is the address these routes are SERVED at, stated by the code that mounts them. It cannot be discovered from r: zip composes definitions, so the same router can be included at more than one site and has no single absolute address until a build resolves the tree. This used to read r.Fiber().(*fiber.Group).Prefix, which answered only because fiber flattened a group into one prefix at declaration — an approximation that goes silently wrong the moment a definition is composed twice.

A stated address is a claim, so it is CHECKED rather than trusted: TestMintRoutesMatchWhatIsServed compares MintRoutes() against the app's own declaration and fails if this prefix does not name real routes.

Register it on a router that has ALREADY resolved the caller — PlatformOnly reads what TokenRequired sets and only ever NARROWS (see platformonly.go).

func MintOp added in v1.49.32

func MintOp[In, Out any](on zip.Router, method, path string, fn zip.TypedHandler[In, Out], opts ...zip.OpOption)

MintOp declares a TYPED op on a mint router: gated and recorded, the same single declaration `mint.Post(path, h)` makes for an untyped route, and projected into the OpenAPI document, the MCP tool list, the CLI and the op-call plane like any other typed op.

It is a function rather than a method because Go methods cannot take type parameters — the same reason zip.Post is one. Pass the mint router as `on`:

middleware.MintOp(mint, http.MethodPost, "/deposit", Deposit)

A money route that is not typed is invisible to every projection, which is why this exists; a money route that is typed but declared with a bare zip.Post is gated (see [mintRouter.OpScope]) but missing from MintRoutes, which is why this is the one to use.

func Namespace

func Namespace() zip.Handler

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

func NotFoundHandler

func NotFoundHandler() zip.Handler

Serve custom 404 page.

fiber returns fiber.ErrNotFound from c.Next() when no route matched; that is the ONE signal for a framework 404. A handler that renders its own 404 (via http.Fail) returns nil, so it is left untouched — only the unmatched-route case is replaced with the custom page.

func OverrideRequestMethod

func OverrideRequestMethod(c *zip.Ctx, method string) error

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

func ParseToken

func ParseToken(c *zip.Ctx) error

ParseToken is the route-middleware form: extract, stash, continue.

func PlatformOnly added in v1.46.31

func PlatformOnly() zip.Handler

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(): membership in the reserved "admin" org (HOME owner=="admin", from the gateway/EdgeAuth X-User-Owner header), NOT any spoofable boolean.

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 PlatformOnlyMW added in v1.49.39

func PlatformOnlyMW(next zip.Handler) zip.Handler

PlatformOnlyMW is the SAME decision in middleware shape, for zip's typed-op path (Router.OpScope). An op's continuation is the `next` it is handed, NOT fiber's chain — the op's handler is wrapped inside this middleware rather than chained after it. A gate that continues with c.Next() therefore finds nothing left to run and yields 404, which the caller then reports as an error, so an AUTHORIZED request to a typed mint op never reaches its handler at all. Proven by TestMintOpScope_AuthorizedOpRunsHandlerOnce.

One decision, two shapes — deliberately not two copies. This is a money-mint gate; a second implementation is a second thing to get wrong, and the failure is silent authorization rather than a compile error.

func RequestContext added in v1.35.2

func RequestContext() zip.Handler

RequestContext extracts the standard Go context from the HTTP request and stores it in the request-scoped locals 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 straight from c.Context(). Authorization (PlatformOnly / settled payment / server-fixed grant) rides on top.

func RequireAdmin added in v1.42.51

func RequireAdmin(c *zip.Ctx) 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 global admin is also allowed (superset). Cross-tenant/platform 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 RequirePlatformAdmin added in v1.46.44

func RequirePlatformAdmin(c *zip.Ctx) bool

RequirePlatformAdmin gates a handler on MayReadPlatform, writing a 403 and returning false when the caller is not a trusted platform principal. It is the in-handler boundary for every cross-org god-view: the route-level TokenRequired(permission.Admin) is a NO-OP on the IAM path (it short-circuits for any IAM-authenticated request without checking the Admin bit), so a handler must call this as its first line — never trust the route gate alone.

func GetRevenue(c *zip.Ctx) error {
	if !middleware.RequirePlatformAdmin(c) { return nil }
	...
}

func SetCFCacheTags added in v1.36.4

func SetCFCacheTags(c *zip.Ctx, 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) zip.Handler

func TokenPermits

func TokenPermits(masks ...bit.Mask) zip.Handler

Permissions required to access route

func TokenRequired

func TokenRequired(masks ...bit.Mask) zip.Handler

Parses token, default permissions check

func UnavailableHandler

func UnavailableHandler() zip.Handler

Serve custom 503 page.

Types

type ErrorDisplayer

type ErrorDisplayer func(c *zip.Ctx, message string, err error)

type MintRoute added in v1.49.0

type MintRoute struct {
	Method string
	Path   string
}

MintRoute is one money-mint route as registered: the HTTP method and the full path fiber routes on (e.g. {POST, "/v1/billing/deposit"}). It is the exported shape of the mint surface — see MintRoutes.

func MintRoutes added in v1.49.0

func MintRoutes() []MintRoute

MintRoutes returns every route declared through Mint, sorted and deduplicated — the mint surface, DERIVED from the registrations themselves rather than hand-listed. It is exported for cross-service checks: cloud's /v1/billing bridge forwards with the admin COMMERCE_SERVICE_TOKEN, which satisfies MayMintMoney, so its forwardable allowlist MUST stay disjoint from this set.

It reports what has been REGISTERED in this process: entries appear as Route() runs, so call it once the routes are registered. Registering the same route again is idempotent (the registry is a set), and the paths are full paths, so a caller comparing against a subtree should match on that subtree's prefix.

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