routers

package
v1.801.218 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package routers holds cross-cutting request filters that run on the unified cloud binary's edge, AFTER the identity boundary (SanitizeIdentity) has resolved the caller's principal — so every gate keys on a VALIDATED IAM owner claim, never a raw client header.

paywall.go is the subscription gate. When enforcement is ON, a request to a gated /v1 product route from a validated org that has NO active paid plan is refused with 402 subscription_required, steering the caller to the upgrade page. Every route the sign-in / billing / plans / model-catalog / health surface needs to SELL and SERVICE that upgrade stays open, so the gate can never lock a user out of paying.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Paywall

func Paywall(enforced func() bool, plans PlanChecker) zip.Handler

Paywall returns the subscription-gate middleware.

enforced is READ PER REQUEST, not once at mount. That is the whole point: the value comes from the cockpit switch an owner flips at admin.hanzo.ai, so enforcement turns on and off within one flag-cache TTL with no redeploy and no CR edit. Evaluating it at mount time — as this did — meant the only way to flip the gate was to restart the binary, and it also meant the switch already registered in the cockpit governed nothing. A nil func is the dark default: a pure passthrough, ZERO behavior change.

This package deliberately does NOT import the flag engine. It takes a func and stays a pure request filter, so the caller decides where the answer comes from.

NOT YET WIRED to the cockpit, and the reason is structural: clients/flags imports the ROOT package (cloud.Deps/Handle/OrgStore/...), and the root package imports this one to mount the middleware — so root can never import flags, and no edge filter mounted from serve.go can read a switch. That is why the `paywall_enforced` switch registered in the cockpit governs clients/entitlements.RequireProduct (a leaf, which may import flags) and NOT this middleware, even though this one is what serve.go actually mounts. Closing that gap means either inverting flags off the root package, or moving enforcement onto RequireProduct at the gated route groups. Until then serve.go passes the PAYWALL_ENFORCED config value and a flip still needs a redeploy.

plans is the commerce plan read; nil ⇒ fail open (commerce cannot answer, so the gate never blocks). It is resolved from deps.Commerce by the caller (serve.go) via a type-assertion to PlanChecker.

Decision order — a request is ADMITTED unless the ONE definitive deny fires:

  1. !enforced ................................ admit (dark ship).
  2. non-/v1 path (SPA shell, static assets, /healthz, /readyz, /zap) ................ admit (the paywall gates the /v1 product API only — never the app that renders the upgrade prompt).
  3. allow-listed /v1 path (auth, billing, plans, models, health, entitlements) .... admit (the sell/service surface).
  4. no validated principal ................... admit (an anonymous caller is the route's own 401/403 to make; a 402 upgrade prompt is meaningless to someone not signed in, and the org is untrusted anyway).
  5. platform super-admin ..................... admit (operator bypass).
  6. org unresolved / plans nil / plans error admit (fail open — never lock out).
  7. org HAS a live paid plan ................. admit.
  8. otherwise ................................ 402 subscription_required.

Types

type PlanChecker

type PlanChecker interface {
	ActivePaidPlan(ctx context.Context, org string) (tier string, paid bool, err error)
}

PlanChecker is the ONE commerce read the paywall needs: does org X hold a LIVE (active or trialing) PAID plan, and which tier. It is a consumer-defined interface (idiomatic Go) satisfied structurally by the co-resident commerce client (clients/commerceclient.ActivePaidPlan) — an OPTIONAL capability resolved from deps.Commerce by type-assertion (mirrors types.ModelLister), so the narrow types.CommerceClient interface is untouched and a commerce build that cannot answer (split-deploy / disabled stub) yields a nil PlanChecker → the paywall fails OPEN.

  • (tier, true, nil) — a live paid plan; admit.
  • ("", false, nil) — resolved, NO live paid plan; the 402 case.
  • (_, _, err) — machinery failure; admit (fail open — never lock out).

Jump to

Keyboard shortcuts

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