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
- Variables
- func ActivePatternZoneOriginTx(ctx context.Context, tx *sql.Tx, tenantID string) (string, bool, error)
- func CanonicalizeHost(input string) (string, bool)
- func DKIMSignerForDomain(ctx context.Context, db *sql.DB, domain string) (sdid, selector, privPEM string, ok bool, err error)
- func DomainCoveredByZone(ctx context.Context, db *sql.DB, slug, domain string) (bool, error)
- func EnsureSystemHostnameTx(ctx context.Context, tx *sql.Tx, tenantID, stack, suffix, now string) (string, error)
- func EnsureZoneHostnameTx(ctx context.Context, tx *sql.Tx, tenantID, stack, origin, now string) (string, error)
- func GenerateDKIM() (privPEM, pubB64 string, err error)
- func IsDevLocalHostname(canonical string) bool
- func IsValidHostname(canonical string) bool
- func LoadCronTimezone(ctx context.Context, q rowQueryer, tenantID string) (tz string, ok bool, err error)
- func MintHandle(stack string) string
- func NewHostnameID() string
- func NewRecordID() string
- func NewZoneID() string
- func PutCronTimezoneTx(ctx context.Context, tx *sql.Tx, tenantID, tz, updatedAt, updatedBy string) error
- func PutDNSSettingsTx(ctx context.Context, tx *sql.Tx, s DNSSettings) error
- func RandLabel() string
- func ReservedSlug(slug string) bool
- func SanitizeSlugHint(s string) string
- func StackLabel(stack string) string
- func TenantForMailZone(ctx context.Context, db *sql.DB, domain string) (slug string, ok bool, err error)
- func ValidDNSRecordType(t string) bool
- type Challenge
- type CronSettingsRow
- type DNSRecord
- type DNSSettings
- type DNSZone
- type Hostname
- type Store
- func (s *Store) ActiveChallenge(ctx context.Context, hostnameID, method string) (*Challenge, error)
- func (s *Store) AttachHostname(ctx context.Context, hostname, stack string) error
- func (s *Store) AttachHostnameTx(ctx context.Context, tx *sql.Tx, hostname, stack string) error
- func (s *Store) Create(ctx context.Context, t Tenant) error
- func (s *Store) CreateChallenge(ctx context.Context, hostnameID, method, createdBy, token string) (Challenge, error)
- func (s *Store) CreateHostname(ctx context.Context, h Hostname) (Hostname, error)
- func (s *Store) CreateHostnameTx(ctx context.Context, tx *sql.Tx, h Hostname) error
- func (s *Store) CreateRecordTx(ctx context.Context, tx *sql.Tx, r DNSRecord) error
- func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, t Tenant) error
- func (s *Store) CreateZoneTx(ctx context.Context, tx *sql.Tx, z DNSZone) error
- func (s *Store) List(ctx context.Context) ([]Tenant, error)
- func (s *Store) ListHostnames(ctx context.Context, tenantID string, includeRevoked bool) ([]Hostname, error)
- func (s *Store) ListRecords(ctx context.Context, zoneID string) ([]DNSRecord, error)
- func (s *Store) ListZones(ctx context.Context, tenantID string, includeRevoked bool) ([]DNSZone, error)
- func (s *Store) Lookup(ctx context.Context, tenantID string) (*Tenant, error)
- func (s *Store) LookupActiveHostname(ctx context.Context, hostname string) (Hostname, error)
- func (s *Store) LookupActiveZone(ctx context.Context, tenantID, origin string) (DNSZone, error)
- func (s *Store) LookupBySlug(ctx context.Context, slug string) (*Tenant, error)
- func (s *Store) LookupChallengeByToken(ctx context.Context, token string) (*Challenge, error)
- func (s *Store) MarkHostnameVerified(ctx context.Context, hostnameID string, when time.Time) error
- func (s *Store) MarkHostnameVerifiedTx(ctx context.Context, tx *sql.Tx, hostnameID string, when time.Time) error
- func (s *Store) RecordChallengeAttempt(ctx context.Context, id, lastError string, verified bool) error
- func (s *Store) RevokeHostname(ctx context.Context, hostname string) error
- func (s *Store) RevokeHostnameTx(ctx context.Context, tx *sql.Tx, hostname string) (canonical string, revokedAt time.Time, err error)
- func (s *Store) RevokeRecordTx(ctx context.Context, tx *sql.Tx, zoneID, name, rtype string) error
- func (s *Store) RevokeZoneTx(ctx context.Context, tx *sql.Tx, tenantID, origin string) (string, error)
- type Tenant
- type Verifier
Constants ¶
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.
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>.
const DefaultTenantID = "tnt_default"
DefaultTenantID is the seeded primary key. Stable so callers can reference it without a slug round-trip.
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.
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?".
const SystemTenantID = "tnt_sys"
SystemTenantID is the system tenant's seeded primary key.
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.
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 ¶
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.
var ErrChallengeExpired = errors.New("challenge expired")
ErrChallengeExpired signals the active challenge for (hostname, method) has aged past its expires_at. Caller should re-issue.
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.
var ErrNotFound = errors.New("not found")
ErrNotFound mirrors the same sentinel the auth registry uses; callers in admin handlers translate it to HTTP 404.
var ErrTokenMismatch = errors.New("token_mismatch")
ErrTokenMismatch signals the response body (or TXT record) didn't match the expected token.
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.
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 ¶
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:
- An EXACT chassis-minted structured host (`tenant_hostnames` with a per-host key) → d=<host> — per-host reputation isolation.
- 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
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
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
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 ¶
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 ¶
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
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 ¶
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
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 StackLabel ¶ added in v0.2.4
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
ValidDNSRecordType reports whether t is a Phase-1/2 supported type.
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
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
// 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
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 ¶
Store is the thin façade over the tenants table. Backed by runtime.db.
func (*Store) ActiveChallenge ¶
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 ¶
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 ¶
AttachHostnameTx is the tx-accepting variant of AttachHostname. Returns ErrNotFound when no active row matches the canonical hostname.
func (*Store) Create ¶
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 ¶
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 ¶
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
CreateRecordTx inserts an override/extra record under a zone. Caller supplies r.ID and a validated, zone-owned r.ZoneID.
func (*Store) CreateTx ¶
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
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) 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
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 ¶
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 ¶
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
LookupActiveZone returns the active zone for (tenantID, origin), or ErrNotFound. Used to authorize record writes against the caller's zone.
func (*Store) LookupBySlug ¶
LookupBySlug resolves a slug to a tenant. Used by the admin mux when it sees `/v1/tenants/{slug}/…`.
func (*Store) LookupChallengeByToken ¶
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 ¶
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 ¶
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
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 ¶
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 ¶
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".