control

package
v0.13.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package control is the mAPI-ng control plane: the Postgres-backed store of orgs (tenants), ingest keys, members, and plan limits. Ingest validates keys against it (cached) and resolves the tenant; the guardrail layer reads a tenant's plan limits from it. It sits above guardrail in the data flow (control depends on guardrail for the shared Limits type, never the reverse) and it is never imported by ingest — main wires control.Resolver in as an ingest.KeyResolver, so ingest stays control-agnostic.

Index

Constants

This section is empty.

Variables

View Source
var ErrKeyNotFound = errors.New("control: ingest key not found or already revoked")

ErrKeyNotFound is returned by RevokeKey when no active key matches the org and id (already revoked, or never existed).

Functions

This section is empty.

Types

type KeyInfo

type KeyInfo struct {
	ID        string
	Label     string
	Last4     string
	CreatedAt time.Time
	RevokedAt *time.Time // nil while the key is active
}

KeyInfo is a listed ingest key for the dashboard key panel. It never carries the secret — only the display-only last-4 fragment and lifecycle timestamps.

type MigrationSource

type MigrationSource struct {
	FS  fs.FS
	Dir string
}

MigrationSource is a directory of ordered .sql files applied on top of the embedded core schema. It is the extension seam for a composed build to add its own tables/columns without editing core migrations: the files must be additive and idempotent, exactly like core.

type Option

type Option func(*options)

Option configures New.

func WithExtraMigrations

func WithExtraMigrations(fsys fs.FS, dir string) Option

WithExtraMigrations registers an additional migration source applied, in lexical filename order, AFTER the embedded core migrations. Multiple sources apply in registration order. This lets a composed build layer extra schema on top of core without forking the core migration history.

type Resolver

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

Resolver resolves an ingest key to a tenant against the control plane, with an in-memory TTL cache. It structurally satisfies ingest.KeyResolver (the error-free Resolve(ctx, key) (string, bool)) so main can pass it in without ingest importing control.

Fail-closed policy: on a database error with no usable cache entry, Resolve returns ok=false (rejects the key) and logs loudly but rate-limited. Auth safety is chosen over availability here: a Postgres blip must not let unknown keys through. A still-fresh cache entry is honored during the outage.

func NewResolver

func NewResolver(pool *pgxpool.Pool, log *slog.Logger) *Resolver

NewResolver builds a Resolver backed by a control-plane pool. The lookup is the real parameterized query; the cache is empty.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, key string) (string, bool)

Resolve returns the tenant for key, hitting the cache first and falling back to the control-plane lookup. See the type doc for the fail-closed policy.

type ServiceOnboarding

type ServiceOnboarding struct {
	Service     string
	Instance    string
	HandshakeAt time.Time
}

ServiceOnboarding is one connected (service, instance) of a tenant and when it was first seen, feeding the dashboard's live onboarding panel (CONTEXT Handshake). HandshakeAt is first_seen so the panel can show how long a source has been connected without yet sending a Summary.

type Store

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

Store wraps a pgx connection pool over the control-plane schema. New pings the database and applies the embedded migration idempotently on startup; all queries are parameterized.

func New

func New(ctx context.Context, dsn string, opts ...Option) (*Store, error)

New opens a pool against dsn, verifies connectivity, and applies the core migrations idempotently, followed by any sources registered via WithExtraMigrations. The caller owns the returned Store and must Close it.

func (*Store) Close

func (s *Store) Close()

Close releases the underlying pool.

func (*Store) DevOrgAdmin

func (s *Store) DevOrgAdmin(ctx context.Context, devOrgName string) (orgID, memberID string, err error)

DevOrgAdmin ensures an admin member exists in the seeded dev org and returns it, for the dev-login auth mode (control plane present but no OIDC provider configured). It resolves the dev org by name (created by EnsureDevKey) and upserts a synthetic admin member keyed on a fixed dev oidc_subject, so repeat dev-logins reuse the same member. Idempotent.

func (*Store) EnsureDevKey

func (s *Store) EnsureDevKey(ctx context.Context, key, orgName string) (tenant string, err error)

EnsureDevKey creates (or reuses) an org named orgName and attaches an ingest key hashing to key, if not already present. It is a dev-seed convenience so local dev keeps working against a real control plane. It is idempotent: a re-run with the same key is a no-op that returns the tenant.

func (*Store) IssueKey

func (s *Store) IssueKey(ctx context.Context, orgID, label string) (secret string, err error)

IssueKey creates a new active ingest key for orgID and returns its plaintext secret exactly once — only sha256(secret) and a display last-4 are stored. The caller wraps the secret with the deployment origin via token.Encode before showing it to the user (control does not know the server's public URL).

func (*Store) Limits

func (s *Store) Limits(ctx context.Context, tenant string) (guardrail.Limits, error)

Limits resolves tenant's plan-limits budget: it reads the org's plan and returns the plan_limits row for that plan. It resolves only the plan budget — any further per-tenant policy lives in an injectable LimitProvider a composing build supplies, not here. It falls back to guardrail.DefaultLimits when the org or its plan row is missing, so a misconfigured plan never fails ingest open on the limit budget.

func (*Store) ListKeys

func (s *Store) ListKeys(ctx context.Context, orgID string) ([]KeyInfo, error)

ListKeys returns an org's ingest keys (active and revoked), newest first. The secret is never returned — only the display last-4 and lifecycle timestamps.

func (*Store) OnboardingState

func (s *Store) OnboardingState(ctx context.Context, tenant string) ([]ServiceOnboarding, error)

OnboardingState returns every connected (service, instance) of tenant ordered by first_seen, so the dashboard can render the live onboarding progress. An empty slice means no source has handshaked yet (still on step 1: key valid).

func (*Store) Pool

func (s *Store) Pool() *pgxpool.Pool

Pool exposes the underlying pool, used by main to build a Resolver and to close the pool during shutdown.

func (*Store) RecordHandshake

func (s *Store) RecordHandshake(ctx context.Context, tenant, service, instance, sdkVersion string) error

RecordHandshake upserts the handshake for (tenant, service, instance), refreshing last_seen and sdk_version and preserving first_seen. It is called from the ingest Register path after successful auth (via the HandshakeRecorder adapter in main), so the dashboard's step 2 (service connected) reflects reality as soon as a recorder pings. Errors are the caller's to log-and- continue: a control-plane write must never fail the handshake itself (the ping's job is proving auth + connectivity, not persistence).

func (*Store) RevokeKey

func (s *Store) RevokeKey(ctx context.Context, orgID, keyID string) error

RevokeKey marks an active key revoked. It returns ErrKeyNotFound if no active key matches (already revoked, wrong org, or unknown id).

func (*Store) UpsertMemberFromOIDC

func (s *Store) UpsertMemberFromOIDC(ctx context.Context, oidcSubject, email string) (orgID, memberID, role string, isNew bool, err error)

UpsertMemberFromOIDC resolves the member behind an OIDC identity, creating an org-of-one on first login. oidcSubject is the provider-prefixed stable id (e.g. "github:12345"); email is the verified primary email.

On first login (no member with this oidcSubject) it creates, atomically in one transaction, an org (plan 'free') and an admin member in it — a solo user is an org of one and the first user is that org's admin (CONTEXT Member / roles, Tenant). On a repeat login it resolves the existing member and returns its org/role unchanged. Returns the org id (the tenant the dashboard renders), the member id, and the member's role.

Jump to

Keyboard shortcuts

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