Documentation
¶
Overview ¶
Package entitlements is the per-org product-enablement plane for the unified Hanzo Cloud binary: the /v1/orgs/:org/entitlements surface the console's paid- product sidebar reads to decide which products to SHOW, and org owners / super admins write to TURN a product on or off.
Surface (all org-scoped; /v1 only):
GET /v1/orgs/:org/entitlements -> { "enabled": ["engine","chat",...] }
POST /v1/orgs/:org/entitlements { "add":[...], "remove":[...] } -> { "enabled":[...] }
TWO AUTHORITIES, NEVER BRAIDED.
- ENABLEMENT (this store): which products the org has toggled on. The org's intent. Durable per-org SQLite ({DataDir}/entitlements.db), (org,product) key.
- ENTITLEMENT (commerce): which products the org's plan/subscription grants. The billing truth. Read via deps.Commerce.CheckEntitlement at WRITE time.
A product may only be ENABLED if it is ENTITLED — so a non-super-admin can only switch on what the org already pays for; enabling never spends new money (a plan upgrade happens in commerce, not here). DISABLING is always allowed (turning a product off is never gated). A SUPER ADMIN (owner==AdminOrg) BYPASSES the commerce gate — the operator can comp/grant any product to any org — and may target ANY :org.
ORG SCOPING mirrors clients/kms (/v1/kms/orgs/:org): {:org} must equal the caller's VALIDATED org (c.Org()), unless the caller is a super admin (c.IsAdmin(), minted only for owner==AdminOrg by SanitizeIdentity — never client-forgeable), who may act on any org. A bearer-less forge (X-Org-Id restored, no X-User-Id) fails the principal.Validated gate → 403. There is no path a caller reads or writes another org's entitlements.
Index ¶
- func Enforced() bool
- func Mount(app cloud.Router, deps cloud.Deps) error
- func RequireProduct(commerce cloud.CommerceClient, product string) zip.Handler
- func Shutdown(_ context.Context) error
- type Store
- func (s *Store) Apply(ctx context.Context, org string, add, remove []string, by string, atUnix int64) ([]string, error)
- func (s *Store) Close() error
- func (s *Store) Disable(ctx context.Context, org, product string) error
- func (s *Store) Enable(ctx context.Context, org, product, by string, atUnix int64) error
- func (s *Store) List(ctx context.Context, org string) ([]string, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Enforced ¶
func Enforced() bool
Enforced reports whether the paywall is on, read live from the cockpit switch (store -> PAYWALL_ENFORCED -> false). It is the ONE reader of enforceKey outside this file's own gate: serve.go hands it to routers.Paywall so the middleware and RequireProduct can never disagree about whether enforcement is on.
func RequireProduct ¶
func RequireProduct(commerce cloud.CommerceClient, product string) zip.Handler
RequireProduct is the unified-paywall enforcement middleware a gated route group applies. It admits the caller when EITHER commercial leg holds — an ACTIVE @hanzo/plans entitlement for `product`, OR a positive prepaid balance to burn down — deferring entirely to the two authorities that own those facts (commerce.CheckEntitlement and the finance ledger). Both legs compose in ONE place, standing.Resolve; this middleware only applies the verdict.
DECISION ORDER — a request is ADMITTED unless the one proven refusal fires:
- enforcement OFF ....................... admit. The kill switch, read FIRST so a dark gate costs one map lookup and touches no authority.
- reachable path ........................ admit, unconditionally. The path to payment is never gated (see `reachable`).
- unvalidated principal ................. 403. An identity refusal, never an infra one — there is no trustworthy org to gate on, and the restored X-Org-Id on the bearer-less path is a forge. Mirrors principal.Org's contract, which every other data-plane gate in this binary already applies.
- platform super-admin .................. admit. Platform sudo is strictly tighter than any purchasable tier; it is not a product and never 402s.
- subscribed OR funded .................. admit.
- UNRESOLVABLE standing ................. posture (`paywall_strict`), logged.
- proven unpaid ......................... 402 with the actionable refusal.
FAIL POSTURE — argued, not inherited. The refusal in (7) requires PROOF: both authorities answered and both said no (standing.Resolve). An authority we could not reach yields Unknown, and by default an Unknown ADMITS. That is not "unpaid users get everything free whenever commerce hiccups" — it is "we never call a customer delinquent on evidence we do not have". The asymmetry is what decides it: refusing on an outage 402s every PAYING customer at once (a total product outage, a support and churn event, irreversible), while admitting on an outage leaks access for the duration of that outage (bounded, recoverable, and separately capped — actual spend still runs through the metering gate and the rolling AI-spend cap, which have their own fail-closed posture). Every fail-open admit logs at WARN with the reason, so a sustained leak is a paging condition rather than a silent one. And when an owner wants the other trade, `paywall_strict` flips it live — the posture is a decision made under observation, not a constant.
Apply it ONLY to route groups whose routes are ALL authenticated: step (3) refuses an unvalidated caller, so a genuinely public route must never be wrapped.
Types ¶
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the entitlements metastore over one SQLite file ({DataDir}/entitlements.db). Org-scoping is the (org, product) key.
func (*Store) Apply ¶
func (s *Store) Apply(ctx context.Context, org string, add, remove []string, by string, atUnix int64) ([]string, error)
Apply enables `add` and disables `remove` for org in ONE transaction, so a batch mutation is all-or-nothing (the console never sees a half-applied set). It returns the resulting enabled set. add/remove are already product-shape validated and entitlement-gated by the caller; a product in BOTH lists is removed (remove wins — the explicit "off" is the safer resolution).
func (*Store) Disable ¶
Disable turns product off for org (idempotent — disabling an already-off product is a no-op, never an error).
func (*Store) Enable ¶
Enable turns product on for org (idempotent — re-enabling keeps the original enabled_at, refreshing only who last enabled it). `by` is the actor's user id, recorded for audit; empty is allowed (e.g. a super-admin machine grant).