tenants

package
v0.2.15 Latest Latest
Warning

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

Go to latest
Published: Jun 26, 2026 License: MPL-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package tenants owns the tenants and tenant_hostnames tables in runtime.db.

Tenants are routing topology — slug, name, and the per-hostname mapping the data-plane router uses to resolve `Host: foo.local` → (tenant_id, stack). They live in runtime.db so a data-plane-only chassis can resolve them without opening auth.db.

Identity-side tables in auth.db (memberships, invitations, browser sessions, etc.) reference tenant_id as opaque TEXT; the cross-DB integrity is by-convention since SQLite cannot enforce FKs across files without ATTACH DATABASE.

Index

Constants

View Source
const ChallengeTTL = 24 * time.Hour

ChallengeTTL is how long a freshly-issued challenge stays valid. After expiry, /verify returns "expired, re-issue first." 24h matches the operator expectation set by ACME and is long enough for one DNS propagation cycle.

View Source
const DKIMSelector = "txco"

DKIMSelector is the fixed selector for chassis-issued DKIM keys. The key material differs per zone (per domain); the selector name is shared. The DNS head publishes the public key at <DKIMSelector>._domainkey.<origin>.

View Source
const DefaultTenantID = "tnt_default"

DefaultTenantID is the seeded primary key. Stable so callers can reference it without a slug round-trip.

View Source
const DefaultTenantSlug = "default"

DefaultTenantSlug is seeded by db/schema/sqlite/runtime/0002_tenants.sql. The first bootstrap actor claims it via the super_admin path.

View Source
const SystemStructuredHostCreatedBy = "system:structured-host"

SystemStructuredHostCreatedBy is the created_by sentinel stamped on chassis-minted hostname rows. It marks a row as managed (so list/UX can flag it, `hostnames remove` can refuse it, and a config-disable won't touch operator vanity rows) and is the idempotency key for "does this stack already have an auto-minted hostname?".

View Source
const SystemTenantID = "tnt_sys"

SystemTenantID is the system tenant's seeded primary key.

View Source
const SystemTenantSlug = "_sys"

SystemTenantSlug owns the chassis ingress-fallback namespace (`boot/*`). Requests that match no ingress route are stamped with this tenant and run pinned to it, so the data-plane op lookup is tenant-filtered like every other request (no global/unfiltered path). A `_sys` boot rule may re-tenant a request into a real tenant — the only place the request's pinned tenant may change, and only one-way (see processor's re-tenant gate). Seeded by db/schema/sqlite/runtime/0007_system_tenant.sql.

View Source
const SystemZoneHostCreatedBy = "system:dns-zone-host"

SystemZoneHostCreatedBy marks a tenant_hostnames row auto-minted for a stack under the tenant's delegated DNS zone (`stack-name.<origin>`). Distinct from SystemStructuredHostCreatedBy so the global-suffix and delegated-zone mechanisms coexist without colliding.

Variables

View Source
var ErrAddressNotPublic = errors.New("address_not_public")

ErrAddressNotPublic signals an SSRF defense rejection: the hostname resolves to one or more non-public IPs and AllowPrivateAddresses is false.

View Source
var ErrChallengeExpired = errors.New("challenge expired")

ErrChallengeExpired signals the active challenge for (hostname, method) has aged past its expires_at. Caller should re-issue.

View Source
var ErrHostnameInUse = errors.New("hostname in use")

ErrHostnameInUse signals that an active row already exists for the requested hostname (partial unique index collision). The caller translates this to HTTP 409 and exposes the prior owner's tenant_id via the conflict body.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound mirrors the same sentinel the auth registry uses; callers in admin handlers translate it to HTTP 404.

View Source
var ErrTokenMismatch = errors.New("token_mismatch")

ErrTokenMismatch signals the response body (or TXT record) didn't match the expected token.

View Source
var ErrUnexpectedRedirect = errors.New("unexpected_redirect")

ErrUnexpectedRedirect signals that the HTTP-01 target returned a 3xx. We never follow — a malicious server could 302 us into an internal URL, defeating the pinned dialer.

View Source
var ErrZoneExists = errors.New("dns zone already exists")

ErrZoneExists signals an active dns_zones row already covers the origin.

Functions

func ActivePatternZoneOriginTx added in v0.2.4

func ActivePatternZoneOriginTx(ctx context.Context, tx *sql.Tx, tenantID string) (string, bool, error)

ActivePatternZoneOriginTx returns the tenant's active pattern-mode zone origin (lexically first when several exist), or ok=false. Read inside the activation tx so the minted routing host is consistent with the same transaction's view of the zone set.

func CanonicalizeHost

func CanonicalizeHost(input string) (string, bool)

CanonicalizeHost parses a Host-header-or-similar string into the canonical form used as the lookup key in `tenant_hostnames`. Returns (canonical, ok). On any ambiguity (too many colons, empty result, leading/trailing punctuation that can't be cleaned up) it returns ok=false — the caller chooses whether that's a hard error (admin write) or a silent miss (resolver read).

Both the admin write path and the resolver read path call this function so a request with Host: "EXAMPLE.com:8080" matches a stored row keyed on "example.com". Centralised here because the cases are fiddly (`[::1]:8080`, trailing dot, mixed case, `host:bad:port`) and duplicating the logic across read/write is exactly the kind of place lookups silently diverge.

v1 does NOT IDNA/punycode. Non-ASCII input is rejected at the admin write boundary by `IsValidHostname`, and on the resolver path simply misses (falling through to boot/%/0).

func DKIMSignerForDomain added in v0.2.6

func DKIMSignerForDomain(ctx context.Context, db *sql.DB, domain string) (sdid, selector, privPEM string, ok bool, err error)

DKIMSignerForDomain returns the signing material for `domain`: the DKIM d= (SDID), selector, and private-key PEM. ok=false when nothing covers it. Resolution order:

  1. An EXACT chassis-minted structured host (`tenant_hostnames` with a per-host key) → d=<host> — per-host reputation isolation.
  2. Else the most-specific delegated zone (`dns_zones` longest-match) → d=<zone origin>.

Used by the sendmail op.

func DomainCoveredByZone added in v0.2.6

func DomainCoveredByZone(ctx context.Context, db *sql.DB, slug, domain string) (bool, error)

DomainCoveredByZone reports whether `domain` (apex or any subdomain) falls under an active dns_zones row for tenant `slug`. Being authoritative for a domain's DNS (NS delegated to us) IS proof of control, so this counts as verified — no separate challenge needed. Package-level (takes *sql.DB) so the mail op and the ingress resolver can both call it. Reads, never the hot-path snapshot.

func EnsureSystemHostnameTx

func EnsureSystemHostnameTx(ctx context.Context, tx *sql.Tx, tenantID, stack, suffix, now string) (string, error)

EnsureSystemHostnameTx makes sure the (tenantID, stack) pair has an active chassis-minted hostname row, creating one if absent. It runs inside the caller's activation transaction (tx) so the row is atomic with the version flip; the caller owns commit and dbcache reload.

Idempotent: re-activations reuse the existing row (the URL never churns). Returns the canonical hostname (without scheme). suffix is the configured apex, e.g. ".stacks.example.com" or ".localhost" — a missing leading dot is tolerated. An empty suffix means the feature is off → ("", nil), a no-op (the caller also guards this).

func EnsureZoneHostnameTx added in v0.2.4

func EnsureZoneHostnameTx(ctx context.Context, tx *sql.Tx, tenantID, stack, origin, now string) (string, error)

EnsureZoneHostnameTx makes sure (tenantID, stack) has an active routing hostname at `<StackLabel(stack)>.<origin>` — the SAME label the dns head synthesizes, so the resolved name and the HTTP/mail route match. Deterministic (no random suffix; the zone is tenant-scoped) and pre-verified (we are authoritative for the zone). Idempotent per (tenant, stack). Runs in the activation tx.

func GenerateDKIM added in v0.2.6

func GenerateDKIM() (privPEM, pubB64 string, err error)

GenerateDKIM creates an RSA-2048 DKIM keypair: the private key as PKCS#1 PEM (the signer parses this) and the public key as base64-encoded PKIX DER (the `p=` tag the DNS head publishes). Called once per zone on the control plane (CreateZoneTx); the result fleet-syncs on the zone row.

func IsDevLocalHostname added in v0.2.4

func IsDevLocalHostname(canonical string) bool

IsDevLocalHostname reports whether canonical is a hostname that a dev-mode chassis can auto-verify on claim, skipping the DNS-TXT proof-of-ownership round-trip. The matcher is conservative:

  • `localhost` — RFC 6761, must resolve to loopback
  • `*.localhost` — RFC 6761 §6.3
  • `*.local` — RFC 6762 mDNS (Bonjour); a developer-machine convention
  • `*.local.thanks.computer` — wildcard A record we publish to 127.0.0.1 so any developer can use `<feature>.local.thanks.computer` without touching /etc/hosts

IP literals (127.0.0.1, ::1) are rejected upstream by IsValidHostname, so they never reach this predicate. Public TLDs (.com, .org, ...) and any operator-owned domain are never matched.

Caller is responsible for passing an already-canonicalized host (call CanonicalizeHost first). The match is case-sensitive on the canonical form.

func IsValidHostname

func IsValidHostname(canonical string) bool

IsValidHostname is the strict admin-write predicate. It's called AFTER CanonicalizeHost, so input is already lowercased / port- stripped / trailing-dot-stripped. Rejects IP literals and other shapes that pass canonicalisation but shouldn't be stored.

func LoadCronTimezone added in v0.2.9

func LoadCronTimezone(ctx context.Context, q rowQueryer, tenantID string) (tz string, ok bool, err error)

LoadCronTimezone returns the tenant's configured cron timezone (an IANA name like "Asia/Tokyo"). ok=false when no row exists or the timezone is empty — callers then treat the tenant as UTC. Works on the live runtime DB or the dbcache snapshot.

func MintHandle

func MintHandle(stack string) string

MintHandle builds the label for a stack's auto-minted hostname. Hinted form ("<sanitized-stack>-<rand>") per the design doc; for the opaque alternative, return randLabel() alone (one-line swap).

func NewHostnameID

func NewHostnameID() string

NewHostnameID generates the canonical thn_ prefixed id callers pass into CreateHostnameTx. Lives here so the producer hook + the Store agree on the format.

func NewRecordID added in v0.2.4

func NewRecordID() string

func NewZoneID added in v0.2.4

func NewZoneID() string

NewZoneID / NewRecordID mint the canonical prefixed surrogate IDs.

func PutCronTimezoneTx added in v0.2.9

func PutCronTimezoneTx(ctx context.Context, tx *sql.Tx, tenantID, tz, updatedAt, updatedBy string) error

PutCronTimezoneTx upserts the tenant's cron timezone. An empty tz clears it (the head falls back to UTC). The caller supplies updatedAt (RFC3339 UTC) so the row matches the fleet-sync artifact uploaded before the tx, and validates the zone name (the admin endpoint does, via time.LoadLocation) so a bad zone fails the set rather than every tick.

func PutDNSSettingsTx added in v0.2.4

func PutDNSSettingsTx(ctx context.Context, tx *sql.Tx, s DNSSettings) error

PutDNSSettingsTx upserts the singleton dns_settings row.

func RandLabel added in v0.2.4

func RandLabel() string

RandLabel exposes randLabel for callers that need a fresh always-alphanumeric DNS label (e.g. a fallback tenant slug when no usable hint can be derived).

func ReservedSlug

func ReservedSlug(slug string) bool

ReservedSlug reports whether a slug is reserved and may not be claimed by a tenant. The whole `_`-prefixed namespace is reserved for chassis-internal tenants (e.g. `_sys`) so operator-owned system tenants can never collide with or be impersonated by a created tenant. Caller should pass the already-normalised (lower/trim) slug.

func SanitizeSlugHint added in v0.2.4

func SanitizeSlugHint(s string) string

SanitizeSlugHint exposes sanitizeHint for callers outside the package (e.g. the cloud-enroll handler deriving a suggested tenant slug from an OIDC subject). Same rules: lowercase, non-alnum runs → '-', trimmed, truncated; may return "".

func SetZoneVerifiedTx added in v0.2.10

func SetZoneVerifiedTx(ctx context.Context, tx *sql.Tx, tenantID, origin, now string) error

SetZoneVerifiedTx stamps verified_at (and bumps updated_at, so the synthesized SOA serial advances) on a tenant's active zone — flipping a PENDING zone live once its NS delegation is confirmed. Idempotent (re-verify refreshes it). ErrNotFound if no active zone for (tenantID, origin).

func StackLabel added in v0.2.4

func StackLabel(stack string) string

StackLabel is the deterministic leftmost DNS label for a stack within a per-tenant delegated DNS zone — the sanitized stack name with NO random suffix (the zone is tenant-scoped, so the label is already unique). Shared by the synthesized DNS pattern (chassis/server/personality/dns) and the activation-path routing-host mint so the resolved name and the routing hostname never diverge. Returns "" for an all-punctuation stack name; callers skip those.

func TenantForMailZone added in v0.2.6

func TenantForMailZone(ctx context.Context, db *sql.DB, domain string) (slug string, ok bool, err error)

TenantForMailZone returns the slug of the tenant whose active zone covers `domain` (apex or subdomain), choosing the MOST SPECIFIC zone when several match (longest origin wins, so sub.example.com beats example.com). Used by the ingress resolver as a fallback when no tenant_hostnames row exists: "we serve DNS for it" ⟹ route mail to <slug>/_mail.

func ValidDNSRecordType added in v0.2.4

func ValidDNSRecordType(t string) bool

ValidDNSRecordType reports whether t is a Phase-1/2 supported type.

func ZoneNSVerified added in v0.2.10

func ZoneNSVerified(ctx context.Context, origin string, wantNameservers []string) (ok bool, resolved []string, err error)

ZoneNSVerified reports whether `origin`'s NS records — resolved RECURSIVELY against the public DNS, not via our own authoritative head — include all of `wantNameservers`, i.e. the domain is actually delegated to us. This makes "delegation is proof of control" real rather than assumed, so a zone can't be squatted (you can't make a domain you don't own delegate to us). Returns the resolved NS hosts (normalized) for diagnostics. Uses net.Resolver{PreferGo: true} like VerifyDNS so cgo/host-file quirks can't satisfy it.

Types

type Challenge

type Challenge struct {
	ID          string
	HostnameID  string
	Method      string
	Token       string
	CreatedAt   time.Time
	CreatedBy   string
	ExpiresAt   time.Time
	AttemptedAt *time.Time
	LastError   string
	VerifiedAt  *time.Time
	RevokedAt   *time.Time
}

Challenge mirrors a tenant_hostname_challenges row — the proof-of- ownership artifact created by `POST /hostnames/{host}/challenges`.

type CronSettingsRow added in v0.2.9

type CronSettingsRow struct {
	Timezone  string
	UpdatedAt string
	UpdatedBy string
}

CronSettingsRow is a tenant's full cron_settings row — used by the fleet resync producer to re-publish the current value verbatim.

func LoadCronSettings added in v0.2.9

func LoadCronSettings(ctx context.Context, q rowQueryer, tenantID string) (CronSettingsRow, bool, error)

LoadCronSettings returns the full cron_settings row for a tenant. ok=false when no row exists (the tenant never configured a timezone). Unlike LoadCronTimezone, a present-but-empty timezone still returns ok=true — an explicitly-cleared row is real state worth re-asserting on resync.

type DNSRecord added in v0.2.4

type DNSRecord struct {
	ID        string
	ZoneID    string
	Name      string
	Type      string
	TTL       sql.NullInt64
	Rdata     string
	CreatedAt string
	CreatedBy string
	UpdatedAt string
	RevokedAt string
}

DNSRecord is one override/extra record within a zone.

type DNSSettings added in v0.2.4

type DNSSettings struct {
	Nameservers []string
	EdgeIPs     []string
	MXHost      string
	MXPriority  int
	SynthTTL    int
	UpdatedAt   string
	UpdatedBy   string
}

DNSSettings is the chassis-global synthesis infrastructure config — the nameservers customers delegate to, the edge A/AAAA target, and the mail exchanger. Singleton per chassis. List fields are stored comma-separated (matching the --dns-* flag convention).

func LoadDNSSettings added in v0.2.4

func LoadDNSSettings(ctx context.Context, q rowQueryer) (DNSSettings, bool, error)

LoadDNSSettings reads the singleton dns_settings row. found=false when no row exists yet (first run) — callers then fall back to the boot `--dns-*` flag defaults.

type DNSZone added in v0.2.4

type DNSZone struct {
	ID         string
	TenantID   string
	Origin     string
	MName      string
	RName      string
	Refresh    int
	Retry      int
	Expire     int
	Minimum    int
	DefaultTTL int
	Mode       string
	CreatedAt  string
	CreatedBy  string
	UpdatedAt  string
	RevokedAt  string
	// VerifiedAt gates whether the zone confers authority (0019). Empty/NULL =
	// pending (created with --dns-require-zone-verification on, awaiting an NS
	// check); set = verified. When the flag is off, CreateZoneTx stamps it at
	// creation so behavior is unchanged.
	VerifiedAt string
	// Per-domain DKIM material (0016), generated once on the control plane at
	// CreateZoneTx and fleet-synced on this row. Populated by GetZoneByIDTx
	// (for the producer); ListZones/LookupActiveZone leave them zero.
	DKIMSelector   string
	DKIMPrivatePEM string
	DKIMPublicB64  string
}

DNSZone is a delegated zone. Timestamps are RFC3339 UTC text (empty RevokedAt = active).

func GetZoneByIDTx added in v0.2.6

func GetZoneByIDTx(ctx context.Context, tx *sql.Tx, id string) (DNSZone, error)

GetZoneByIDTx loads a single zone (active or revoked) by id from the tx, so a producer can read the fully-defaulted persisted row — SOA timers + timestamps filled by CreateZoneTx/RevokeZoneTx — to fleet-publish it. Returns ErrNotFound if absent.

type Hostname

type Hostname struct {
	ID         string
	Hostname   string
	TenantID   string
	Stack      string
	CreatedAt  time.Time
	CreatedBy  string
	RevokedAt  *time.Time
	VerifiedAt *time.Time
	// Per-host DKIM material (0017), set only on chassis-minted structured
	// hosts (created_by = SystemStructuredHostCreatedBy). Empty for custom
	// domains (which use their dns_zones key). Carried by the loaders + the
	// fleet row builder so per-host signing/publishing stays consistent.
	DKIMSelector   string
	DKIMPrivatePEM string
	DKIMPublicB64  string
}

Hostname mirrors a tenant_hostnames row.

type Store

type Store struct {
	DB *sql.DB
}

Store is the thin façade over the tenants table. Backed by runtime.db.

func New

func New(db *sql.DB) *Store

New builds a Store against the given runtime *sql.DB.

func (*Store) ActiveChallenge

func (s *Store) ActiveChallenge(ctx context.Context, hostnameID, method string) (*Challenge, error)

ActiveChallenge returns the live (non-verified, non-revoked) challenge for (hostnameID, method). Returns ErrNotFound when none exists, ErrChallengeExpired when the row exists but its expires_at has passed.

func (*Store) AttachHostname

func (s *Store) AttachHostname(ctx context.Context, hostname, stack string) error

AttachHostname sets the stack on an existing active row, identified by canonical hostname. Stack must be non-empty (call RevokeHostname to remove the row, or pass an empty stack to detach in a future version). Returns ErrNotFound if no active row matches.

The caller is responsible for verifying that the stack exists in the same tenant — the store doesn't reach into the stacks table.

func (*Store) AttachHostnameTx

func (s *Store) AttachHostnameTx(ctx context.Context, tx *sql.Tx, hostname, stack string) error

AttachHostnameTx is the tx-accepting variant of AttachHostname. Returns ErrNotFound when no active row matches the canonical hostname.

func (*Store) Create

func (s *Store) Create(ctx context.Context, t Tenant) error

Create inserts a new tenants row. Caller supplies the pre-generated tenant_id (hxid "tnt_…") and a slug. Slug is lower-cased and trimmed before insert because the UNIQUE index is case-sensitive.

func (*Store) CreateChallenge

func (s *Store) CreateChallenge(ctx context.Context, hostnameID, method, createdBy, token string) (Challenge, error)

CreateChallenge issues a fresh challenge for (hostnameID, method). Any existing active challenge for that pair is soft-revoked first — both moves happen in one BEGIN IMMEDIATE transaction so the partial unique index never sees two active rows.

`tokenGen` is the source of the secret token (160 bits). Passed in so tests can pin it. Production callers use a crypto/rand-backed generator. The token MUST be unique across all challenges (the UNIQUE constraint on tenant_hostname_challenges.token enforces it, but generating 160 bits makes the collision risk negligible).

func (*Store) CreateHostname

func (s *Store) CreateHostname(ctx context.Context, h Hostname) (Hostname, error)

CreateHostname canonicalizes the supplied hostname, generates an hxid "thn_…", and inserts. On collision with an active row (driven by the partial unique index on hostname WHERE revoked_at IS NULL) returns ErrHostnameInUse and stamps the conflicting row's TenantID onto the returned Hostname so the caller can render a 409 with the prior owner's id.

Caller-supplied fields used: Hostname, TenantID, Stack, CreatedBy. Other fields (ID, CreatedAt) are filled in here.

func (*Store) CreateHostnameTx

func (s *Store) CreateHostnameTx(ctx context.Context, tx *sql.Tx, h Hostname) error

CreateHostnameTx is the tx-accepting variant of CreateHostname. Differs in one important way: the caller MUST pre-generate the hostname ID and pass it in via h.ID. (Generating inside the tx would prevent the producer hook from including the ID in its pre-tx artifact JSON.) An empty h.ID returns an error.

On unique-violation the caller has to do their own follow-up lookup — we can't run LookupActiveHostname inside the same tx without an explicit second query, and the original CreateHostname path's "lookup the conflicting owner" affordance isn't useful when the caller is the producer hook (which is publishing authoritative state, not reconciling with an existing row).

func (*Store) CreateRecordTx added in v0.2.4

func (s *Store) CreateRecordTx(ctx context.Context, tx *sql.Tx, r DNSRecord) error

CreateRecordTx inserts an override/extra record under a zone. Caller supplies r.ID and a validated, zone-owned r.ZoneID.

func (*Store) CreateTx

func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, t Tenant) error

CreateTx inserts a tenants row inside the caller's tx. Same validation and SQL as Create. Returns the same errors.

func (*Store) CreateZoneTx added in v0.2.4

func (s *Store) CreateZoneTx(ctx context.Context, tx *sql.Tx, z DNSZone) error

CreateZoneTx inserts a delegated zone inside the caller's tx. The caller MUST supply z.ID (see CreateHostnameTx for the rationale). The origin is canonicalized; SOA timers default when zero. A second active zone for the same origin returns ErrZoneExists.

func (*Store) List

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

List returns every non-revoked tenant, newest first.

func (*Store) ListHostnames

func (s *Store) ListHostnames(ctx context.Context, tenantID string, includeRevoked bool) ([]Hostname, error)

ListHostnames returns hostnames for a tenant. When includeRevoked is false (the default for admin list endpoints) only currently-active rows are returned, ordered by hostname for stable shell-script consumption. When true, revoked rows are included for "who used to own this?" debugging, ordered by created_at DESC so the most recent rows appear first.

func (*Store) ListRecords added in v0.2.4

func (s *Store) ListRecords(ctx context.Context, zoneID string) ([]DNSRecord, error)

ListRecords returns the active records for a zone.

func (*Store) ListZones added in v0.2.4

func (s *Store) ListZones(ctx context.Context, tenantID string, includeRevoked bool) ([]DNSZone, error)

ListZones returns a tenant's zones (active only unless includeRevoked).

func (*Store) Lookup

func (s *Store) Lookup(ctx context.Context, tenantID string) (*Tenant, error)

Lookup reads a tenants row by primary key. Returns ErrNotFound when no row exists; satisfies the registry.TenantLookup interface so the auth registry can resolve slugs without importing this package.

func (*Store) LookupActiveHostname

func (s *Store) LookupActiveHostname(ctx context.Context, hostname string) (Hostname, error)

LookupActiveHostname is the read-side lookup: hostname → (Hostname row, Tenant row). The resolver in chassis/server/ingress does its own JOIN against the dbcache mirror for the data-plane hot path; this method is for admin handlers that want the same shape without re-implementing the JOIN.

func (*Store) LookupActiveZone added in v0.2.4

func (s *Store) LookupActiveZone(ctx context.Context, tenantID, origin string) (DNSZone, error)

LookupActiveZone returns the active zone for (tenantID, origin), or ErrNotFound. Used to authorize record writes against the caller's zone.

func (*Store) LookupBySlug

func (s *Store) LookupBySlug(ctx context.Context, slug string) (*Tenant, error)

LookupBySlug resolves a slug to a tenant. Used by the admin mux when it sees `/v1/tenants/{slug}/…`.

func (*Store) LookupChallengeByToken

func (s *Store) LookupChallengeByToken(ctx context.Context, token string) (*Challenge, error)

LookupChallengeByToken is the read-side lookup for the /.well-known/txco-verify/{token} handler. Returns the active challenge for the token, ErrNotFound if no row matches, or ErrChallengeExpired if the matching row exists but is past its expires_at.

func (*Store) MarkHostnameVerified

func (s *Store) MarkHostnameVerified(ctx context.Context, hostnameID string, when time.Time) error

MarkHostnameVerified flips the parent row's verified_at to `when`. Called after RecordChallengeAttempt with verified=true so the resolver can see the hostname as verified on its next dbcache reload.

func (*Store) MarkHostnameVerifiedTx

func (s *Store) MarkHostnameVerifiedTx(ctx context.Context, tx *sql.Tx, hostnameID string, when time.Time) error

MarkHostnameVerifiedTx is the tx-accepting variant of MarkHostnameVerified.

func (*Store) RecordChallengeAttempt

func (s *Store) RecordChallengeAttempt(ctx context.Context, id, lastError string, verified bool) error

RecordChallengeAttempt updates attempted_at + last_error after a verification attempt. When verified is true, the row's verified_at is also set to now (and last_error cleared); callers should follow with MarkHostnameVerified to flip the parent row.

func (*Store) RevokeHostname

func (s *Store) RevokeHostname(ctx context.Context, hostname string) error

RevokeHostname soft-deletes the active row matching the canonical hostname. Idempotent — revoking an absent or already-revoked hostname returns nil with no row affected.

func (*Store) RevokeHostnameTx

func (s *Store) RevokeHostnameTx(ctx context.Context, tx *sql.Tx, hostname string) (canonical string, revokedAt time.Time, err error)

RevokeHostnameTx is the tx-accepting variant of RevokeHostname. Idempotent; reports the canonical hostname and the timestamp it stamped so the caller can build a consistent fleet-sync artifact even when the underlying row was already revoked (in which case no rows are affected but the timestamp returned is when the call was made — the producer's view of the moment).

func (*Store) RevokeRecordTx added in v0.2.4

func (s *Store) RevokeRecordTx(ctx context.Context, tx *sql.Tx, zoneID, name, rtype string) error

RevokeRecordTx soft-revokes active records matching (zoneID, name, type). Returns ErrNotFound when none matched.

func (*Store) RevokeZoneTx added in v0.2.4

func (s *Store) RevokeZoneTx(ctx context.Context, tx *sql.Tx, tenantID, origin string) (string, error)

RevokeZoneTx soft-revokes a tenant's active zone by origin. Lenient: an already-absent zone returns ErrNotFound. Records under it stop serving once the zone is gone from the snapshot (zones are read WHERE revoked_at IS NULL).

type Tenant

type Tenant struct {
	TenantID  string
	Slug      string
	Name      string
	CreatedAt time.Time
	RevokedAt *time.Time
}

Tenant mirrors the tenants table row.

type Verifier

type Verifier struct {
	// AllowPrivateAddresses bypasses the SSRF blocklist when running
	// HTTP-01 against a hostname that resolves to a private,
	// loopback, link-local, or otherwise non-public address. Default
	// false (production-safe); `txco dev` flips it true in
	// devDefaults so localhost workflows function.
	AllowPrivateAddresses bool
}

Verifier runs hostname-ownership checks for the admin /verify endpoint. Two methods are supported:

  • DNS TXT: look up `_txco-verify.<hostname>` and match any TXT value against the challenge token. Works before the hostname's A/CNAME points at the chassis.
  • HTTP-01: build a per-attempt http.Client with a pinned-IP dialer and no redirect-following, then fetch `http://<hostname><port>/.well-known/txco-verify/<token>` and compare the body to the token.

Both methods accept a context and apply their own timeouts on top. All errors returned from this package are safe to surface verbatim to the operator (they describe what was attempted, not what the chassis saw internally).

func (*Verifier) VerifyDNS

func (v *Verifier) VerifyDNS(ctx context.Context, hostname, token string) error

VerifyDNS resolves `_txco-verify.<hostname>` TXT records and matches any value (after TrimSpace) against either "txco-verify=<token>" or the bare "<token>". Uses `net.Resolver{PreferGo: true}` so /etc/hosts and cgo-only resolvers don't accidentally satisfy verification in dev.

func (*Verifier) VerifyHTTP

func (v *Verifier) VerifyHTTP(ctx context.Context, hostname, portSuffix, token string) error

VerifyHTTP fetches `http://<hostname><portSuffix>/.well-known/ txco-verify/<token>` with SSRF defenses (pinned-IP dialer + redirect refusal) and confirms the response body equals the token. `portSuffix` is "" for default port 80, ":<n>" otherwise — production behind an LB on :80 uses "", dev with the chassis on :8080 uses ":8080".

Jump to

Keyboard shortcuts

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