Documentation
¶
Overview ¶
Package featuregate is the launch-control GATE for Hanzo's hosted services — the COMPLETE waitlist feature, COMPOSING the ONE flag engine (clients/flags) one-way. It owns:
- the host→service registry (registry.go) + the brand seed (waitlist.go),
- the per-service MODE decide WaitlistModeForHost — a service's mode IS the switch waitlist.<svc>, evaluated through the flag engine (flags.Bool),
- the admin control funcs (List/Set/Upsert) the /v1/admin/services board calls,
- the guard's public mode read /v1/flags/waitlist (+ /v1/featuregate/mode compat), Mount,
- the native enforcement middleware (Enforce, this file),
- the per-user approval predicate (Approvals, reused from IAM — approval.go).
flags NEVER imports featuregate; featuregate imports flags. The engine is the pure (Principal, context) -> verdict primitive; this package is its first composed tenant. Enforcement is decomplected into two orthogonal axes:
- PER-SERVICE waitlist mode on|off — the switch waitlist.<svc>, resolved for a request host via WaitlistModeForHost (the decide, waitlist.go).
- PER-USER approvalStatus pending|approved — owned by IAM (approval.go), REUSED.
THE RULE, applied at ONE native enforcement point (Enforce):
if waitlistMode[host] AND NOT user.approved → bounce to the waitlist if approved OR mode=off → allow unauthenticated → login first
Index ¶
- Variables
- func Enforce(cfg EnforceConfig) zip.Handler
- func Mount(app *zip.App, deps cloud.Deps) error
- func NormalizeHost(host string) string
- func Shutdown() error
- func WaitlistModeForHost(ctx context.Context, host string) (mode bool, service string, known bool)
- type Approvals
- type EnforceConfig
- type SeedService
- type ServiceInput
- type ServiceRow
- type ServiceView
Constants ¶
This section is empty.
Variables ¶
var ErrServiceNotFound = errors.New("featuregate: waitlist service not found")
ErrServiceNotFound is returned when a service slug is not in the registry.
Functions ¶
func Enforce ¶
func Enforce(cfg EnforceConfig) zip.Handler
Enforce builds the native enforcement middleware. It is a no-op passthrough when the decide reports the host is not governed (gate known=false — the flags registry not mounted yet, a store error, or an un-governed host), so a request before boot completes is never wrongly gated.
func Mount ¶
Mount installs the launch-control gate: it opens the platform-tenant host→service registry, seeds it for the deployment brand, registers a waitlist.<svc> switch per service in the flag engine (flags.Register), and serves the guard's public mode read at the CURRENT frozen path (/v1/flags/waitlist) plus the /v1/featuregate/mode compat alias. Fail-safe: a registry error (e.g. cek master key not yet injected) degrades to the in-memory seed switches — WaitlistModeForHost then fail-opens. Mounts AFTER flags so the engine's platform-switch plane is installed first.
func NormalizeHost ¶
NormalizeHost reduces a request Host to the registry key: lowercased, trimmed, port stripped. ONE canonicalization for the seed, onboard, and every lookup, so "Hanzo.Chat:443" and "hanzo.chat" resolve to the same service.
func WaitlistModeForHost ¶ added in v1.801.59
WaitlistModeForHost is THE decide the Enforce consumer, /v1/flags/waitlist, and the admin board call: resolve host→service, then read the waitlist.<svc> switch through the flag engine. FAIL-OPEN by construction — an unmounted registry, a store error, or an un-governed host all return known=false, so a request is NEVER gated pre-boot or on a registry fault (availability over a hard gate, matching the guard).
Types ¶
type Approvals ¶
type Approvals struct {
// contains filtered or unexported fields
}
Approvals resolves whether the current caller is off the waitlist. It is the ONE approval predicate the native middleware uses, DRY with the @file waitlist-guard (both read properties.approvalStatus == "pending"). Resolution order:
- global admin (c.IsAdmin()) → approved (admins are never gated)
- validated header X-User-Approved → its bit (forward-perfect, no lookup)
- IAM get-account (caller's creds) → approved unless approvalStatus=="pending" — cached per user for ttl; FAIL-OPEN on any IAM error.
func NewApprovals ¶
NewApprovals builds a resolver. iamBase is the in-cluster IAM base (e.g. http://iam.hanzo.svc.cluster.local:8000); ttl bounds the per-user cache. A zero iamBase yields a resolver whose lookup always fails-open (approved) — safe for a deployment where approval is enforced elsewhere (the guard).
type EnforceConfig ¶
type EnforceConfig struct {
// WaitlistURL is where an unapproved / unauthenticated browser is bounced
// (per-brand, e.g. https://waitlist.hanzo.ai). Empty → API-style 403/401 for
// everyone (no redirect target), so enforcement still holds.
WaitlistURL string
// Approvals resolves whether the caller is off the waitlist. When nil, Enforce
// builds one from IAMBase.
Approvals *Approvals
// IAMBase is the in-cluster IAM base used to build Approvals when it is nil.
IAMBase string
// ExemptPrefixes are request-path prefixes never gated (health/metrics/auth).
// A sensible default set is used when empty.
ExemptPrefixes []string
// Gate is THE decide: it resolves whether a request host is in waitlist mode,
// via the ONE policy engine. When nil it is WaitlistModeForHost —
// host→service→waitlist.<svc>. Injected only in tests. Fail-open by contract:
// known=false (unmounted / registry error / un-governed host) → not gated.
Gate func(ctx context.Context, host string) (mode bool, service string, known bool)
}
Enforce is the NATIVE, forward-perfect enforcement point for the waitlist — the single in-binary middleware that reads the registry (in-process, no HTTP hop) and the caller's approval, and applies THE RULE for every request whose Host is a governed service. As product hosts fold into the one-binary cloud, this is the ONE enforcement point (the @file waitlist-guard is the interim gate for hosts not yet cloud-fronted; both read the SAME registry so a toggle governs both).
THE RULE (per request, on a governed host in waitlist mode):
carries a Hanzo API key (hk-/sk-/…) → allow (paid inference; possession-gated) exempt path (health/iam/waitlist) → allow unauthenticated → 302 waitlist (browser) / 401 (API) waitlist mode OFF (or host un-governed) → allow (c.Next) waitlist mode ON AND approved → allow (c.Next) waitlist mode ON AND NOT approved → 302 waitlist (browser) / 403 (API)
INTEGRATION POINT — wire in serve.go RIGHT AFTER SanitizeIdentity:
app.Use(IdentityMiddleware(cfg)) // establishes the validated principal
app.Use(featuregate.Enforce(featuregate.EnforceConfig{ WaitlistURL: … })) // ← here
It reads the sanitized X-User-Id / X-User-IsAdmin / X-User-Approved that IdentityMiddleware minted, so it MUST run after it and (like BillingGate) before the subsystem handlers. It is deliberately NOT wired here — the unified-binary agent owns serve.go's boot chain; this package exposes Enforce so the one-line app.Use lands without a merge collision. The decide (WaitlistModeForHost) is resolved PER REQUEST and fail-opens until the flags engine has mounted, so Enforce can be constructed before Mount runs.
WHY NATIVE IS CANONICAL (in-cluster-bypass). The @file edge guard only gates traffic arriving THROUGH the ingress — a pod reaching another service's pod directly in-cluster bypasses it (a cluster-wide baseline NetworkPolicy + Cilium broad-allow union means additive netpols can't seal that). For a waitlist (threat model = external users) edge-only is acceptable, but this native middleware is FORWARD-PERFECT: when the app IS cloud, the gate is IN the request path, so reaching the pod directly STILL hits it — there is no edge to go around. That is the reason the native middleware is the canonical enforcement and the @file guard is purely interim. It is also STATELESS (it reads the sanitized X-User-* headers, sets no cookie), so the multi-apex cookie-domain concern the @file guard must handle does not exist here at all.
Paths that must NEVER be gated (health, the waitlist page's own API, auth callbacks) are skipped via ExemptPrefixes so enforcement can't lock the platform out of its own recovery/observability surface.
type SeedService ¶
SeedService is one row of the launch registry (a hosted service + its hosts). Mode is intentionally absent — the launch posture (gated) is waitlistDef's Default "true".
type ServiceInput ¶ added in v1.801.59
type ServiceInput struct {
Service string `json:"service"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Hosts []string `json:"hosts"`
WaitlistMode bool `json:"waitlistMode"`
}
ServiceInput is the admin onboard/edit payload for /v1/admin/services. WaitlistMode sets the launch switch for a NEW service; a re-register PRESERVES the live switch.
type ServiceRow ¶ added in v1.801.59
type ServiceRow struct {
Service string `json:"service"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Hosts []string `json:"hosts"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
}
ServiceRow is one hosted service in the registry (host→service + metadata). The waitlist MODE is intentionally absent — it is the platform switch waitlist.<svc>, read through the engine; ListWaitlistServices composes the two into a ServiceView.
type ServiceView ¶ added in v1.801.59
type ServiceView struct {
ServiceRow
WaitlistMode bool `json:"waitlistMode"`
}
ServiceView is one service as the admin board renders it: the registry row plus its LIVE waitlist mode (the waitlist.<svc> switch evaluated through the engine).
func ListWaitlistServices ¶ added in v1.801.59
func ListWaitlistServices(ctx context.Context) ([]ServiceView, error)
ListWaitlistServices returns the admin board: every registered service with its LIVE mode (the waitlist.<svc> switch). SuperAdmin surface (the caller gates).
func SetWaitlistMode ¶ added in v1.801.59
func SetWaitlistMode(ctx context.Context, service string, mode bool, actor string) (ServiceView, error)
SetWaitlistMode flips one service's waitlist switch — the launch lever — and returns the updated view. It is the ONE write path (through flags.SetPlatformSwitch, audited in the flag activity log); the flip is hot (this pod applies immediately, peers converge within the eval TTL). ErrServiceNotFound when the slug is unknown.
func UpsertWaitlistService ¶ added in v1.801.59
func UpsertWaitlistService(ctx context.Context, in ServiceInput, actor string) (ServiceView, error)
UpsertWaitlistService onboards or edits a hosted service so a new host is governed WITHOUT a redeploy. A NEW service takes in.WaitlistMode as its launch mode; a re-register PRESERVES the live switch (never silently re-gating an opened service).