entitlements

package
v1.801.490 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package entitlements is what your org may run: what the plan grants, and which of those products are switched on.

Both authorities live here, one door each, and the package's whole discipline is that they are never braided.

Surface (/v1 only):

GET  /v1/entitlements             -> per-app booleans from the org's PLAN (projection.go)
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 SQLite — the deployment's own "entitlements" — keyed (org, product).
  • 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 apps/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

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 Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount registers the entitlements surface on app per HIP-0106.

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:

  1. enforcement OFF ....................... admit. The kill switch, read FIRST so a dark gate costs one map lookup and touches no authority.
  2. reachable path ........................ admit, unconditionally. The path to payment is never gated (see `reachable`).
  3. 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.
  4. platform super-admin .................. admit. Platform sudo is strictly tighter than any purchasable tier; it is not a product and never 402s.
  5. subscribed OR funded .................. admit.
  6. UNRESOLVABLE standing ................. posture (`paywall_strict`), logged.
  7. 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.

func Shutdown

func Shutdown(_ context.Context) error

Shutdown releases the entitlements store. Idempotent.

Types

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the entitlements metastore over one SQLite file (the deployment's own "entitlements"). 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) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) Disable

func (s *Store) Disable(ctx context.Context, org, product string) error

Disable turns product off for org (idempotent — disabling an already-off product is a no-op, never an error).

func (*Store) Enable

func (s *Store) Enable(ctx context.Context, org, product, by string, atUnix int64) error

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

func (*Store) List

func (s *Store) List(ctx context.Context, org string) ([]string, error)

List returns the enabled product ids for org, sorted (a stable contract the console can diff). An org that has enabled nothing returns an empty slice, not an error — "no products enabled yet" is a valid state, never a 404.

Jump to

Keyboard shortcuts

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