valis / Reference / API reference
Operator state - API reference
Exported surface for the operator-state subsystem. Part of the API reference.
Package valis/src/operator-state/audit
Conditions
schema-audit-failed
Signaled when the schema audit finds a column that denotes message body or header content, falls outside the deliberate allow-list, or is a pointer column whose type could carry bytes. Raised inside the migration unit's transaction so the offending migration rolls back fail-closed.
Generic functions
schema-audit-failed-columns
(schema-audit-failed-columns condition)
Undocumented: this exported symbol needs a docstring.
Functions
address-column-p
(address-column-p name)
Return T when NAME ends in an address suffix — a content-hash pointer/score, an address, NOT content.
audit-migration-schema
(audit-migration-schema &optional version)
Scan the live schema and signal schema-audit-failed naming any forbidden column. Shaped to be called by the migration runner inside each unit's transaction (with the just-applied VERSION) after the unit's DDL runs and before the commit, so a body/header column aborts that migration's transaction fail-closed. Returns the scanned column alist on a clean schema. Runs inside the caller's connection/transaction.
column-definitions
(column-definitions ddl)
Return an alist of (downcased-column-name . upcased-type-and-modifiers) for the top-level COLUMN definitions in DDL, skipping table-level constraint clauses. A quoted identifier ("only") is reduced to its bare name. Pure: parses the DDL source text, touches no database.
content-column-p
(content-column-p name)
Return T when column NAME denotes body or header CONTENT: it carries a content token AND is not an address column. bodyptr (a pointer) is excluded; a bare body / header / payload / content / message column is caught.
forbidden-schema-columns
(forbidden-schema-columns column-defs)
Return the subset of COLUMN-DEFS (an alist of (downcased-name . upcased-type)) the schema may not carry: a column is forbidden when it denotes body/header content by name, falls outside the permitted allow-list, or is an allow-listed pointer column whose type is not a small hash string (a body smuggled under a *ptr name as bytea). An empty result means the schema is clean.
live-schema-columns
(live-schema-columns)
Return an alist of (downcased-column-name . upcased-type) for every column of every table in the connection's active schema search-path — the schema exactly as the migration just applied it. Runs inside the caller's connection/transaction so it sees the open transaction's uncommitted DDL. Scoped to the active schema (the operator's operator-state tables), so it audits valis's own tables and not the PostgreSQL catalog.
Assumes the search-path schema is dedicated to valis's operator-state tables: a co-tenant table sharing the schema would be measured against the allow-list and make every migration fail closed. operator-state runs on its own dedicated seam, so the assumption holds; a deployment that co-locates other tables in the same schema must scope this scan to the valis-managed table set.
Package valis/src/operator-state/config
Classes
site-requirement
One site fact a resident boot cannot start without, declared once and read by everything that needs to know about it.
VARIABLE is the environment variable that names the fact and WHAT says in plain words what that variable is for, so a refusal can tell an operator both. RESOLVER is the symbol naming the reader that answers the fact, called with one argument: a function of a variable name returning its value, or NIL. Passing the lookup in rather than reading the ambient environment is what lets the same declaration be checked against a DIFFERENT process's environment, which is the whole reason a deploy can refuse before it moves anything.
Conditions
operator-state-dsn-malformed
Signaled when the DSN's host= parameter is present but names neither a socket directory nor a host. It carries only the offending value, never the DSN it came from: a TCP DSN can still carry a password, and a condition that printed the whole URI would copy it into the journal.
site-configuration-missing
Signaled when a verb needs a site fact the operator has not named. It carries the environment variable to set and what that variable names, because a refusal that does not say which value is missing leaves the operator to find it by reading source.
Functions
acme-production-enabled-p
(acme-production-enabled-p)
Return T only when the operator has explicitly opted into production ACME issuance via the VALISACMEPRODUCTION environment variable; NIL otherwise (unset, empty, or any non-affirmative value). This is the single gate that keeps the production CA — and its strict rate ceiling — unreachable by default so CI/dev can never place a production order. The env-var seam matches the VALISPGDSN pattern (chosen over ubiquitous to stay with the shipped config).
build-pooled-spec
(build-pooled-spec dsn)
Build the Postmodern connect arglist (positional database/user/password/host plus :port and :pooled-p t) from the connection plist DSN. :pooled-p routes acquisition through the bounded bordeaux pool; the worker binds this via with-connection, acquire-late / release-early.
ensure-edge-acme-store-root
(ensure-edge-acme-store-root)
Make the configured ACME custody-store root exist, and return it. Signals SITE-CONFIGURATION-MISSING when no root is configured, and the underlying file error when the configured root cannot be created.
I have the resident do this once at boot so that an absent root afterwards means a fault and nothing else. mercer's custody enumeration signals on a root it cannot open, which is the right answer for a path that is wrong; but a node that has never obtained a certificate has no root either, and from the filesystem those two are the same observation. Only valis knows which of them it is looking at. Once boot has settled it, an empty root reads as empty custody and a missing root reads as the fault mercer reports it to be.
parse-pg-dsn
(parse-pg-dsn uri)
Parse a PostgreSQL connection URI into a connection plist (:database :user :password :host :port). Accepts the libpq URI shape postgres[ql]://[user[:password]@]host[:port]/database[?params]. Missing host defaults to localhost; missing port to 5432.
A host= query parameter selects the connection endpoint and overrides any host in the authority, which is how a unix-domain socket is spelled:
postgresql://valis@/valisstate?host=/var/run/postgresql postgresql:///valisstate?host=/var/run/postgresql
The resulting host — an absolute path — reaches cl-postgres unchanged, which reads a leading slash as a socket directory rather than a host to resolve. The PORT stays load-bearing on that path: it selects the socket FILE (.s.PGSQL.<port>) in that directory, not a TCP port, so a socket DSN still names one.
A host= that is neither an absolute path nor host-shaped is refused rather than defaulted. Only the parameter is validated, not the authority host, so every DSN that parsed before still parses to the same plist.
require-boot-site-configuration
(require-boot-site-configuration &optional (lookup (function getenv)))
Signal SITE-CONFIGURATION-MISSING for the first declared boot requirement LOOKUP does not answer, and return the number checked when it answers all.
I have the resident run this at the top of its bring-up so that the registry is what decides whether a boot may proceed, not the accident of which reader some later line happens to call first. A requirement declared and then read nowhere on the boot path would otherwise sit in the registry looking like coverage, and the deploy that trusted it would be checking a fact the resident never needed.
require-edge-acme-store-path
(require-edge-acme-store-path)
Return the ACME custody-store root the operator configured, or signal SITE-CONFIGURATION-MISSING when VALISACMESTOREPATH names none.
I refuse a substituted root here more firmly than anywhere else a site fact is missing, because of what a store root holds. It is where the ACME account key is minted and kept, so a store rooted somewhere the operator did not choose registers a fresh account against the CA and spends that CA's registration ceiling. A certificate written to the wrong place can be moved back; a spent registration cannot be unspent. The library's own ambient default is refused for the same reason: it would rest production custody on whatever leftover key material happens to sit in the state directory the process inherited.
require-secondary-transfer-address
(require-secondary-transfer-address)
Return the address the secondary pulls zone transfers from, or signal SITE-CONFIGURATION-MISSING when the operator has not named one.
An enrolment is keyed by that address, so a substituted one authorises a host nobody chose to pull every domain enrolled through it.
require-transfer-master-address
(require-transfer-master-address)
Return this node's public authoritative-DNS address, or signal SITE-CONFIGURATION-MISSING when the operator has not named one.
Used where the address decides what a zone or a pasted BIND fragment says: a substituted address there mints a domain that resolves somewhere nobody chose, and reports success doing it.
resolve-acme-directory-url
(resolve-acme-directory-url)
Return the ACME directory URL the renewal manager should use. Production is returned ONLY when acme-production-enabled-p — the explicit VALISACMEPRODUCTION opt-in; otherwise a VALISACMEDIRECTORYURL override (a Pebble/CI or staging endpoint) is honored when set, and the staging URL is the fail-safe default. So the production CA is unreachable without the opt-in: no environment short of the explicit opt-in yields the production directory, and the default is staging.
resolve-edge-acme-store-path
(resolve-edge-acme-store-path &optional (lookup (function getenv)))
Return an explicit ACME custody-store root the :443 edge loads its certificate from, as a directory pathname from the VALISACMESTOREPATH environment variable, or NIL to use mercer/boomer's own default store location. The renewal manager persists issued certs into the same store; a launcher sets this only when that store is not the default, and a test points it at a scratch store.
LOOKUP reads a variable and defaults to this process's environment. A deploy asking what the node it is about to change will find passes that node's environment instead, and reads the same value by the same rule.
resolve-edge-domain
(resolve-edge-domain)
Return the DNS name the resident's public :443 HTTPS edge serves a certificate for, from the VALISEDGEDOMAIN environment variable, or NIL when it is unset or blank. NIL is the fail-closed default: with no served domain named there is no certificate to load from custody, so the :443 edge stays cert-gated dark. The name is the operator's — the domain the resident terminates TLS for — supplied by the launcher rather than fabricated, so a boot never guesses a served identity.
resolve-edge-domains
(resolve-edge-domains)
Return the DNS names DECLARED to this instance as primary, from VALISEDGEDOMAIN and VALISEDGEDOMAINS together: the base name first, then the further declared names in the order they were written, trimmed, duplicates collapsed case-insensitively, and the empty list when nothing is declared. Blank or whitespace-only values read as unset, so the resolver fails closed the way its neighbours do and never fabricates a name.
VALISEDGEDOMAIN keeps its existing meaning exactly: it is the BASE certificate, the one the edge presents when it cannot choose, and it is always the first member here when set. VALISEDGEDOMAINS only ADDS to the set; neither variable overrides the other, so a deployment that sets only the singular behaves as it does today.
⛔ These are the DECLARED names alone. A domain a user brought reaches the served set by a different route, and the two are combined elsewhere; this resolver reads the environment and nothing else.
⚠ The base is still REQUIRED to open :443. A deployment that declares nothing here but holds an owned zone has no base credential to present, so the public edge stays dark while holding a non-empty name set. That is today's behaviour, unchanged, and it is stated because "the site still does not serve" is otherwise a silent puzzle.
resolve-mail-accept-localparts
(resolve-mail-accept-localparts)
Return the reject-unknown RCPT accept-list from VALISMAILACCEPTLOCALPARTS (a comma/space set of base localparts), or NIL when unset. NIL leaves the RCPT- disposition seam at its built-in default accept-list (the operator's named localparts), so an unset variable never widens acceptance to a catch-all.
resolve-mail-local-domains
(resolve-mail-local-domains)
Return the list of PRIMARY-LOCAL mail domains from VALISMAILLOCALDOMAINS (a comma/space-separated set), or NIL when unset. Threaded into the router's local-domains seam. NIL is fail-closed: with no local domain every recipient is non-local and the open-relay guard governs every relay attempt.
resolve-mail-resolve-ca-file
(resolve-mail-resolve-ca-file)
Return the TLS trust-anchor CA bundle path for the DoT MX upstream, as a pathname from VALISMAILRESOLVECAFILE, or NIL when unset. NIL is an EMPTY trust store, not a bypass — mercer then rejects every peer, so mail defers until an operator names a trust anchor.
resolve-mail-resolve-dot-adn
(resolve-mail-resolve-dot-adn)
Return the authentication-domain-name of the DoT MX upstream (its RFC6125 verify name / SNI) from VALISMAILRESOLVEDOTADN, or NIL when unset.
resolve-mail-resolve-dot-host
(resolve-mail-resolve-dot-host)
Return the DoT upstream host valis resolves recipient MX through, from VALISMAILRESOLVEDOTHOST, or NIL when unset. NIL leaves the mail resolver unbuilt so the drain defers every relay entry (fail-closed — no smarthost).
resolve-mail-secondary-for
(resolve-mail-secondary-for)
Return the authorized backup-MX authority as an ALIST (domain . primary-mx-host) from VALISMAILSECONDARYFOR, or NIL when unset. The value is a comma/space set of `domain=primary-host' tokens, e.g. `example.com=203.0.113.10'. A token with no `=' is skipped (a domain with no primary host is not a usable secondary). Threaded into the router's secondary-for seam.
resolve-operator-state-dsn
(resolve-operator-state-dsn)
Read VALISPGDSN from the environment and parse it into a connection plist, or return NIL when the variable is unset or empty. This is the one place the operator's connection details enter the image.
On the deployed host those details carry no secret: the DSN names a socket directory and a role, and peer authentication supplies the identity from the kernel. The role named here must match the account the process runs as, or the connection is refused.
resolve-secondary-ns
(resolve-secondary-ns)
Return the nameserver name published beside this node's own in every domain's apex NS set, from VALISSECONDARYNS, or NIL when it is unset.
A node may legitimately serve its domains from one nameserver, so NIL means the apex NS set names only the primary and a delegation is expected to name only the primary. Nothing is substituted for an absent secondary: a name nobody runs, published in the NS set of every domain here, is a delegation resolvers chase to a host that answers for none of them.
resolve-secondary-transfer-address
(resolve-secondary-transfer-address)
Return the address the secondary pulls zone transfers from, out of VALISSECONDARYTRANSFERADDRESS, or NIL when it is unset.
Held apart from the secondary's NAME because the serving side matches the two by different things: a pulling secondary presents itself as an address and nothing else, so only an address can match a transfer-allowlist row.
resolve-transfer-master-address
(resolve-transfer-master-address)
Return this node's public authoritative-DNS address from VALISTRANSFERMASTERADDRESS, or NIL when it is unset.
It is the address a secondary points its masters{} clause at, the address an outbound NOTIFY must leave from, and the address a new domain's records resolve to when the operator names no other.
site-requirement-variable
(site-requirement-variable instance)
Undocumented: this exported symbol needs a docstring.
site-requirement-what
(site-requirement-what instance)
Undocumented: this exported symbol needs a docstring.
unmet-boot-site-requirements
(unmet-boot-site-requirements &optional (lookup (function getenv)))
The declared boot requirements LOOKUP does not answer, in declaration order, and the empty list when it answers all of them.
This is the reading a deploy takes of the node it is about to change: LOOKUP is the environment that node's unit will supply, so the answer is what the resident there will find rather than what the machine running the deploy happens to hold. Getting that backwards passes on the deploy host and strands the remote node, which is worse than not checking at all.
Macros
define-boot-site-requirement
(define-boot-site-requirement reader &key variable what resolver documentation)
Declare a site fact a resident boot requires, and define READER as the way to demand it.
One form does both halves. It registers the requirement so a deploy can check the fact against the node it is about to change, and it defines READER, which answers the fact from this process's environment or signals SITE-CONFIGURATION-MISSING naming VARIABLE and WHAT.
⛔ The two halves cannot be added separately, and that is the entire point. A preflight holding its own list of variable names drifts from the code the moment somebody adds a requirement and edits only one of them, and nothing goes red: the deploy passes, the node takes the new binary, and the resident then refuses to boot on a fact the deploy never asked about. Adding a boot requirement here updates both readers of the declaration or neither.
Variables
*acme-production-directory-url*
The production ACME directory URL — the real CA with the strict rate ceiling. Reached ONLY when the operator sets the VALISACMEPRODUCTION opt-in, never by default.
*acme-staging-directory-url*
The default, non-production ACME directory URL. Staging issues untrusted certs against a relaxed rate ceiling — safe for CI/dev, the fail-safe default when no production opt-in is set.
*boot-site-requirements*
Every site fact a resident boot requires, in declaration order.
⛔ Nothing adds to this list by hand. DEFINE-BOOT-SITE-REQUIREMENT registers the entry and defines the reader that refuses without it from the SAME form, so a requirement cannot exist for one of them and not the other. A second list of variable names kept in step by hand is the defect this registry exists to make impossible: it drifts silently, and the drift is only discovered by a deploy that has already stranded a node.
*operator-state-dsn*
The resolved operator-state connection plist (:database d :user u :password p :host h :port n), or NIL when unset. Bound at runtime from the VALISPGDSN environment variable.
:HOST carries either a host to resolve or, when it begins with a slash, the directory holding the PostgreSQL unix-domain socket; :PORT selects the socket file within that directory rather than a TCP port. :PASSWORD is NIL on the socket path, where peer authentication takes the connecting process's OS identity from the kernel and no password exists to hold.
*operator-state-spec*
The pooled Postmodern connect arglist a worker binds via with-connection, or NIL when the subsystem is stopped. Built from operator-state-dsn at start.
Package valis/src/operator-state/domain-template
Functions
apply-record-overrides
(apply-record-overrides records overrides origin)
RECORDS with every authored record whose (owner, type) an OVERRIDES entry also names dropped, then OVERRIDES appended. ORIGIN is the absolute zone origin the owners canonicalise under. Order is preserved for what survives, so the minted zone reads in template order with the operator's substitutions in place.
default-dkim-record-value
(default-dkim-record-value public-key &key (key-type *default-dkim-key-type*))
The DKIM record a domain publishes for one selector: the KEY-TYPE and the PUBLIC-KEY a verifier checks a signature against. Minted only when a signer exists to produce signatures this key verifies.
default-dmarc-policy
(default-dmarc-policy reporting-address &key (disposition *default-dmarc-disposition*))
The DMARC policy a new domain publishes: DISPOSITION for mail that fails authentication, aggregate reports to REPORTING-ADDRESS, and strict alignment on both DKIM and SPF. Strict alignment is what makes the policy mean the domain rather than any subdomain a sender could claim.
default-domain-records
(default-domain-records origin instance-ip &key (serial 1) (primary-ns-label *default-primary-ns-label*) (secondary-ns (resolve-secondary-ns)) (soa-rname-label *default-soa-rname-label*) (soa-refresh *default-soa-refresh*) (soa-retry *default-soa-retry*) (soa-expire *default-soa-expire*) (soa-minimum *default-soa-minimum*) (aliases *default-aliases*) (mail-exchange-label *default-mail-exchange-label*) (mail-exchange-preference *default-mail-exchange-preference*) (spf-qualifier *default-spf-qualifier*) spf-policy (dmarc-disposition *default-dmarc-disposition*) dmarc-reporting-address dmarc-policy dkim-selector dkim-public-key (dkim-key-type *default-dkim-key-type*) default-ttl overrides)
The default record set for a new domain at ORIGIN served from INSTANCE-IP, as a flat sequence of runciter-typed resource records including the apex SOA: exactly what commit-zone-records takes.
The zone is the authority (apex SOA, the apex NS set naming this instance's in-zone primary and, when the operator has named one, the shared secondary, and the primary's glue A), the default surface (an apex A at the instance and one A per alias), mail routing (a single MX), and the outbound email-authentication policy (SPF, DMARC, and DKIM when a key exists).
Every policy choice is a keyword argument. SPF-POLICY, DMARC-POLICY and the DKIM record's value can each be supplied whole, in which case they win outright; left absent, each is composed from the narrower knobs beside it. DMARC-REPORTING-ADDRESS defaults to dmarc@<origin>, a per-domain mailbox; an operator running many domains will usually point them all at one address instead.
No DKIM record is minted unless both DKIM-SELECTOR and DKIM-PUBLIC-KEY are supplied.
SECONDARY-NS defaults to the operator's configured secondary nameserver and is NIL when none is configured, in which case no secondary NS record is minted.
DEFAULT-TTL left absent leaves every record on the zone's SOA MINIMUM, the RFC 2308 fallback, which is how a zone keeps one TTL under one knob.
OVERRIDES is a list of runciter authored records; each REPLACES the template's record of the same owner and type rather than joining it. Nothing here writes, serves, or renders text: it returns typed values.
default-spf-policy
(default-spf-policy instance-ip &key (qualifier *default-spf-qualifier*))
The SPF policy a new domain publishes: mail comes from the hosts this domain's A and MX records name, and from INSTANCE-IP, and QUALIFIER decides what a receiver does with anything else. INSTANCE-IP is named explicitly as well as covered by the a mechanism, so the policy still holds if the apex A is later pointed elsewhere.
Variables
*default-aliases*
The conventional alias labels every new domain gets, each an A record pointing at the same instance as the apex. Kept short on purpose: a name that resolves is a name someone can be sent to, so the set is the ones a domain is actually reached by.
*default-dkim-key-type*
The key type published in a DKIM record when a selector and key are supplied.
*default-dmarc-disposition*
The DMARC policy for mail that fails authentication: none (report only, changing nothing a receiver does), quarantine, or reject.
Report only is instrumentation, not a weak default awaiting correction. A brand new service on new code cannot yet know which of its own legitimate mail will fail authentication against the assorted legacy systems it has to interoperate with, and under quarantine or reject that mail disappears without the domain owner ever learning it existed. None keeps the aggregate reports arriving while changing nothing for a receiver. Monitor, assess the reports, then update the policy. Do not tighten this as a cleanup.
*default-mail-exchange-label*
The label of the mail exchanger within each domain, matching the mail alias.
*default-mail-exchange-preference*
The MX preference of the single mail exchanger. One exchanger needs no ordering; the value leaves room above and below for a second one later.
*default-primary-ns-label*
The label of the primary nameserver within each domain, so the primary NS name is in zone and takes glue from this domain's own A record. In-zone and per-domain rather than one shared name, so a domain moved to another instance moves its nameserver name with it instead of leaving a dangling reference to this one.
*default-soa-expire*
The SOA EXPIRE: how long a secondary keeps answering for a zone it can no longer refresh. Two weeks, long enough to survive an outage over a holiday.
*default-soa-minimum*
The SOA MINIMUM: the negative-caching TTL, and the zone's fallback record TTL when no other TTL is established. Five minutes keeps a mistake cheap to correct.
*default-soa-refresh*
The SOA REFRESH: how long a secondary waits before re-checking the serial when no NOTIFY arrives. An hour is a backstop, not the propagation path: NOTIFY is.
*default-soa-retry*
The SOA RETRY: how long a secondary waits before retrying a refresh that failed.
*default-soa-rname-label*
The label of the SOA responsible-party mailbox within each domain, giving hostmaster@<origin> as the conventional zone contact (RFC 2142).
*default-spf-qualifier*
The qualifier on SPF's terminal all mechanism. A soft fail asks a receiver to accept mail this domain's records do not cover and report it, so the reports name the real senders before any policy starts turning them away.
This value is instrumentation, not a weak default awaiting correction. valis is an entirely new internet service on new code, and nothing yet establishes how it behaves once exposed to the legacy mail systems of the world. A hard fail (-all) discards exactly the evidence needed to find that out, as losses nobody reports. Monitor, assess what the reports show, then update the policy. Do not tighten this as a cleanup.
Package valis/src/operator-state/exemplar
Classes
operator-state-row
The one permanent operator-state dao-class and the canonical row pattern every later domain table copies: a text NAME key, a content-hash body POINTER (the address of a body block, never the bytes — it cannot be inverted to content), and a per-row optimistic GENERATION. Structurally body-never-in-Postgres — no body or header column ever, a shape the sovereignty audit enforces.
Conditions
stale-row
Signaled when a per-row compare-and-swap matched no row — a lost update against a concurrent legitimate writer.
Generic functions
row-body-ptr
(row-body-ptr object)
Undocumented: this exported symbol needs a docstring.
row-generation
(row-generation object)
Undocumented: this exported symbol needs a docstring.
row-name
(row-name object)
Undocumented: this exported symbol needs a docstring.
Functions
operator-state-row-table-ddl
(operator-state-row-table-ddl)
Return the CREATE TABLE statement for the exemplar operator-state table, derived from the dao-class slots so the schema and the DAO never drift. The migration runner and the sovereignty audit both consume this string.
update-operator-state-row
(update-operator-state-row name new-ptr expected-generation my-generation)
Compare-and-swap the body pointer for the row named NAME to NEW-PTR iff its generation still equals EXPECTED-GENERATION, bumping the generation on success — the canonical operator-state write, composing BOTH write guards in ONE transaction.
The two guards are orthogonal and a real mutating write needs both: first assert-fence-touch MY-GENERATION (the live store write-epoch, read per transaction) proves this instance still holds the shared instance fence — a superseded zombie signals fenced-out here and writes nothing, the split-brain stop; then the per-row optimistic CAS guards against a lost update between two legitimate writers. Wrapping both in one logical transaction is what makes the fence assertion binding: a fenced writer that would otherwise win the row CAS is rejected before the CAS runs, and the unit rolls back atomically.
The CAS is an explicit parameterized s-sql :update through postmodern:execute — never a DAO save, which is primary-key based and cannot carry the generation predicate. The affected-row count is the win/lose signal: 1 means the CAS won (returned); 0 means the predicate matched no row — a lost update against a concurrent legitimate writer — and signals stale-row, writing nothing.
A logical transaction opens a real transaction at top level (the production call through with-operator-state-connection) and a savepoint when nested, so the fence assertion and the CAS commit or roll back as one unit in both.
Package valis/src/operator-state/fence
Conditions
fenced-out
Signaled when a mutating transaction's assert-touch matched no fence row — this instance's write-epoch has been superseded by a successor.
Functions
assert-fence-touch
(assert-fence-touch my-generation)
Assert no successor has superseded this instance before it writes, with a no-op self-write of the fence row gated WHERE writeepoch <= MY-GENERATION. When the touch matches the row (one affected) the writer is still legitimate and the transaction proceeds; when it matches nothing (zero affected) the shared fence stands ahead of MY-GENERATION, so signal fenced-out and write nothing. That is the split-brain stop. Runs inside the caller's connection/transaction.
claim-instance-fence
(claim-instance-fence new-generation)
Monotonically advance the instance write-fence to NEW-GENERATION, returning true when this call advanced it and false when the fence already held a generation at least NEW-GENERATION (an idempotent re-claim). The forward-only WHERE writeepoch < NEW-GENERATION makes a re-claim of an already-current epoch a zero-row no-op, so recovery is safe to repeat. Runs inside the caller's connection/transaction; the affected-row count is the advanced/no-op signal.
current-instance-write-epoch
(current-instance-write-epoch)
The write-epoch the shared instance fence currently holds, or NIL when the fence row is absent.
instance-fence-present-p
(instance-fence-present-p)
True when the instance write-fence table exists in the current searchpath.
instance-fence-table-ddl
(instance-fence-table-ddl)
Return the CREATE TABLE statement for the single-row instance write-fence. The single "only" boolean column (double-quoted because ONLY is a PostgreSQL reserved word) as PRIMARY KEY DEFAULT true plus the CHECK constraint force exactly one row; writeepoch holds the store write-epoch this instance projects into the shared medium, defaulting to the genesis epoch 0. The migration runner and the sovereignty audit consume this string.
seed-instance-fence
(seed-instance-fence generation)
Ensure the single fence row exists and monotone-claim it up to GENERATION at subsystem start (genesis seeds at 0). Idempotent: the row is inserted only when absent (the migration seeds it at 0; this covers a bare table), and the claim advances it only when GENERATION is strictly newer, so re-seeding never moves the fence backward. Returns true when this call advanced the fence. Runs inside the caller's connection/transaction.
Package valis/src/operator-state/mail-state
Classes
mail-transport-state
The mail transport-state operator-state row: the envelope + lifecycle + queue position of a message, keyed by its content-derived entry-name, referencing the body only by content-hash POINTER (bodyptr). Structurally body-never-in-Postgres — no body or header column, ever; the bytes stay opaque on the content store. Every column is envelope / delivery metadata; a tier transition is a per-row CAS on the disposition column under the instance fence.
Conditions
stale-mail-state
Signaled when a mail-state per-row compare-and-swap matched no row — a lost update against a concurrent legitimate writer (or a row whose disposition no longer matches the transition's precondition). The transaction writes nothing.
Generic functions
mail-state-attempts
(mail-state-attempts object)
Undocumented: this exported symbol needs a docstring.
mail-state-authenticated-submit
(mail-state-authenticated-submit object)
Undocumented: this exported symbol needs a docstring.
mail-state-body-ptr
(mail-state-body-ptr object)
Undocumented: this exported symbol needs a docstring.
mail-state-disposition
(mail-state-disposition object)
Undocumented: this exported symbol needs a docstring.
mail-state-envelope-from
(mail-state-envelope-from object)
Undocumented: this exported symbol needs a docstring.
mail-state-envelope-from-null
(mail-state-envelope-from-null object)
Undocumented: this exported symbol needs a docstring.
mail-state-envelope-rcpt
(mail-state-envelope-rcpt object)
Undocumented: this exported symbol needs a docstring.
mail-state-generation
(mail-state-generation object)
Undocumented: this exported symbol needs a docstring.
mail-state-max-hops
(mail-state-max-hops object)
Undocumented: this exported symbol needs a docstring.
mail-state-name
(mail-state-name object)
Undocumented: this exported symbol needs a docstring.
mail-state-next-attempt
(mail-state-next-attempt object)
Undocumented: this exported symbol needs a docstring.
mail-state-received-at
(mail-state-received-at object)
Undocumented: this exported symbol needs a docstring.
mail-state-status
(mail-state-status object)
Undocumented: this exported symbol needs a docstring.
mail-state-status-class
(mail-state-status-class object)
Undocumented: this exported symbol needs a docstring.
mail-state-submitter-did
(mail-state-submitter-did object)
Undocumented: this exported symbol needs a docstring.
Functions
active-disposition-count
(active-disposition-count my-generation)
Return the number of rows whose disposition is "active" — the bounded active-window occupancy, read inside the fenced transaction (assert-fence-touch MY-GENERATION first) so the count and any admission decision made on it share one consistent fenced view. There is deliberately no in-image counter; the table IS the count.
admit-mail-active
(admit-mail-active name expected-generation my-generation)
Admit the incoming row named NAME into the bounded active window: a per-row CAS moving disposition "incoming" -> "active" iff the generation still equals EXPECTED-GENERATION, bumping the generation on success. Asserts the instance fence at MY-GENERATION first. Returns 1 affected on success; signals stale-mail-state and writes nothing when no row matches the generation-and-incoming predicate.
advance-mail-state
(advance-mail-state name new-disposition attempts status status-class next-attempt expected-generation my-generation)
Advance the lifecycle state of the row named NAME: a per-row CAS setting the new disposition, attempt count, SMTP status / status-class, and next-attempt instant iff the generation still equals EXPECTED-GENERATION, bumping the generation on success. BODY-PTR is left UNCHANGED — the body never moves. Asserts the instance fence at MY-GENERATION first. Returns 1 affected on success; signals stale-mail-state and writes nothing on a lost update.
mail-state-table-ddl
(mail-state-table-ddl)
Return the CREATE TABLE statement for the mailtransportstate table, derived from the dao-class slots so the schema and the DAO never drift. The migration runner and the sovereignty audit both consume this string.
place-mail-state
(place-mail-state name body-ptr envelope-rcpt my-generation &key envelope-from envelope-from-null (status-class "none") (status "") (received-at 0) authenticated-submit submitter-did (max-hops +default-max-hops+))
Land a fresh mail-state row named NAME into disposition "incoming", idempotently: a re-land of the same content-derived NAME collapses onto the existing row and never creates a duplicate (:on-conflict-do-nothing). Asserts the instance fence at MY-GENERATION first (a superseded writer writes nothing), then inserts. Returns the affected-row count — 1 on a fresh land, 0 on an idempotent re-land. BODY-PTR is the content-hash pointer; the body bytes are never touched here.
Package valis/src/operator-state/migrate
Conditions
migration-dirty-blocked
Signaled by the runner when the ledger carries a dirty row — a crash landed mid-migration and further migration is refused until resolved.
Functions
applied-migration-versions
(applied-migration-versions)
Return the migration versions recorded as applied in the schemamigrations ledger, sorted ascending. A parameterized s-sql :select; the caller diffs this against migrations to find what is still pending.
ensure-schema-migrations
(ensure-schema-migrations)
Bootstrap the schemamigrations ledger when absent. Idempotent: a plain CREATE TABLE IF NOT EXISTS, safe to call on every run before reading the applied versions. The ledger carries the migration version (primary key), the apply timestamp, and the dirty flag the crash guard reads.
register-migration
(register-migration version thunk)
Register THUNK as the migration for integer VERSION, keeping migrations sorted by version ascending; re-registering an existing version replaces it. THUNK runs the migration's DDL inside the runner's per-migration transaction. Return VERSION.
run-pending-migrations
(run-pending-migrations)
Apply every registered migration whose version is not yet recorded, each once, low to high, returning the list of versions newly applied (NIL when nothing was pending — an idempotent no-op).
First bootstrap the ledger and refuse to proceed when any row is dirty: a prior crash landed between mark-dirty and clear-dirty, so further migration is blocked (migration-dirty-blocked, naming the offending version) until an operator resolves it. Then, for each pending unit, inside ONE transaction: INSERT the version with dirty=true, run the unit's DDL, run the confused-deputy schema audit, then UPDATE dirty=false. A crash or a signal anywhere in the unit rolls back the whole transaction — the DDL and its dirty marker together — so the ledger is never left advertising a half-applied unit; the marker only ever reads true for an in-flight transaction a crash discards.
The schema audit runs as a built-in step BEFORE the commit, so a body- or header-bearing column makes the audit signal and rolls THIS migration's transaction back: the migration that introduced the bad column is aborted fail-closed, not merely the next one. The optional post-migration-audit-hook runs in the same pre-commit window as an extra audit seam.
Variables
*migrations*
The ordered numbered migration registry: an alist of (version . thunk), kept sorted by version ascending. register-migration appends here; run-pending-migrations applies each version not yet recorded in the schemamigrations ledger, low to high.
Package valis/src/operator-state/ndb-entry
Classes
also-notify
The also-notify recipient set: additional NOTIFY targets for a zone beyond its apex NS owner-names, keyed by (zone-id, notify-ref). Distinct minimal operator state, never merged with the transfer allowlist — a NOTIFY recipient is not automatically transfer-authorized. notify-ref is a PUBLIC recipient reference only, never key bytes.
ndb-entry
The record-class-agnostic ndb-general anchor (a Plan 9 ndb analog), keyed by (name, kind): the authority/provenance facets — who CONTROLS versus who AUTHORED an entry — the cutover-state, the version, and the crash-safe SOA refresh-timer state. The timer columns are persisted absolute instants, never process uptime, so a restart cannot extend a secondary's expiry window. Every column is public, published-by-design metadata; no key bytes exist here by construction. dnszone is its DNS-typed projection (origin = name for kind "dns-zone").
secondary-peer
The downstream secondary-peer feed allowlist: which secondary peers a zone is fed to, keyed by (zone-id, peer-ref). Kept distinct from the upstream acceptance allowlist and never merged. peer-ref is a public reference (a name or address) only — never key bytes.
transfer-peer-allow
The serving-direction transfer allowlist: which downstream peer, presenting which TSIG key NAME, a zone authorizes an AXFR to, keyed by the (zone-id, peer-ref, key-name) conjunction (NSD provide-xfr model). Kept distinct from the ingest acceptance/feed allowlists and never merged. key-name is the PUBLIC key NAME only — never key bytes; TSIG key custody is mercer's. Absence from this table IS refusal: there is no unauthenticated transfer row.
upstream-master-allow
The upstream-master acceptance allowlist: which masters a zone accepts a secondary transfer from, keyed by (zone-id, master-ref). Kept distinct from the downstream feed allowlist and never merged. master-ref is a public reference only — never key bytes; TSIG key custody is mercer's.
Generic functions
also-notify-notify-ref
(also-notify-notify-ref object)
Undocumented: this exported symbol needs a docstring.
also-notify-zone-id
(also-notify-zone-id object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-authority-state
(ndb-entry-authority-state object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-cutover-state
(ndb-entry-cutover-state object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-expire-deadline
(ndb-entry-expire-deadline object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-kind
(ndb-entry-kind object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-last-refresh-at
(ndb-entry-last-refresh-at object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-name
(ndb-entry-name object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-provenance-kind
(ndb-entry-provenance-kind object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-retry-deadline
(ndb-entry-retry-deadline object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-source-serial
(ndb-entry-source-serial object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-upstream-master
(ndb-entry-upstream-master object)
Undocumented: this exported symbol needs a docstring.
ndb-entry-version
(ndb-entry-version object)
Undocumented: this exported symbol needs a docstring.
secondary-peer-peer-ref
(secondary-peer-peer-ref object)
Undocumented: this exported symbol needs a docstring.
secondary-peer-zone-id
(secondary-peer-zone-id object)
Undocumented: this exported symbol needs a docstring.
transfer-peer-allow-key-name
(transfer-peer-allow-key-name object)
Undocumented: this exported symbol needs a docstring.
transfer-peer-allow-peer-ref
(transfer-peer-allow-peer-ref object)
Undocumented: this exported symbol needs a docstring.
transfer-peer-allow-zone-id
(transfer-peer-allow-zone-id object)
Undocumented: this exported symbol needs a docstring.
upstream-master-allow-master-ref
(upstream-master-allow-master-ref object)
Undocumented: this exported symbol needs a docstring.
upstream-master-allow-zone-id
(upstream-master-allow-zone-id object)
Undocumented: this exported symbol needs a docstring.
Functions
also-notify-table-ddl
(also-notify-table-ddl)
Return the CREATE TABLE statement for the alsonotify recipient set.
ndb-entry-table-ddl
(ndb-entry-table-ddl)
Return the CREATE TABLE statement for the ndbentry anchor, derived from the dao-class slots so the schema and the DAO never drift.
secondary-peer-table-ddl
(secondary-peer-table-ddl)
Return the CREATE TABLE statement for the secondarypeer feed allowlist.
transfer-peer-allow-table-ddl
(transfer-peer-allow-table-ddl)
Return the CREATE TABLE statement for the transferpeerallow serving-direction transfer allowlist.
upstream-master-allow-table-ddl
(upstream-master-allow-table-ddl)
Return the CREATE TABLE statement for the upstreammasterallow acceptance allowlist.
Package valis/src/operator-state/pool
Conditions
operator-state-dsn-unconfigured
Signaled by start-operator-state when no DSN resolves, so a caller that starts the pool with nothing configured fails closed instead of arming an unusable all-nil pooled spec.
operator-state-host-not-local
Signaled when the configured operator-state host is somewhere this node would have to leave its own network to reach.
Distinct from the unreachable condition next door, and deliberately so: that one says a local database did not answer, this one says the configuration asks for a connection this subsystem is not the right way to open. An operator reading the first goes and looks at the database; an operator reading this one has to change where the database is or how it is reached.
operator-state-unreachable
Signaled by assert-operator-state-reachable when the operator-state host does not accept a connection within the probe deadline. Distinct from every other boot failure so a caller can tell 'the database is out of reach' from 'the database answered and something else went wrong' — the two want different operator responses, and a condition that conflated them would send the reader to the wrong place. SECONDS is the measured wait, and CAUSE names the outcome the kernel reported, so a refused connection and a silently-dropped one stay distinguishable in the journal.
Generic functions
operator-state-host-not-local-host
(operator-state-host-not-local-host condition)
Undocumented: this exported symbol needs a docstring.
operator-state-unreachable-cause
(operator-state-unreachable-cause condition)
Undocumented: this exported symbol needs a docstring.
operator-state-unreachable-host
(operator-state-unreachable-host condition)
Undocumented: this exported symbol needs a docstring.
operator-state-unreachable-port
(operator-state-unreachable-port condition)
Undocumented: this exported symbol needs a docstring.
operator-state-unreachable-seconds
(operator-state-unreachable-seconds condition)
Undocumented: this exported symbol needs a docstring.
Functions
assert-operator-state-reachable
(assert-operator-state-reachable &key (deadline *reachability-deadline*) (dsn (or *operator-state-dsn* (resolve-operator-state-dsn))))
Signal operator-state-unreachable unless the operator-state host accepts a TCP connection within DEADLINE seconds. Returns T when the host answers, and when there is nothing to probe: an unconfigured DSN and a local-socket DSN both return T, so a dev or test boot with no TCP database is unaffected.
Run this before the boot's first pooled acquisition. cl-postgres connects blocking and with no deadline of its own, so an operator-state host that drops packets rather than refusing them parks the boot thread in connect(2) for the kernel's full SYN-retry window, long past the point a supervisor gives up on readiness. The unit is then killed mid-connect, before any condition is signalled, so the failure reaches the journal as silence. This probe converts that silence into a signalled condition the caller can act on.
The socket is created, driven and closed here, so the deadline is enforced without interrupting a syscall inside a socket owned by cl-postgres, which offers no handle to close and would leak a descriptor on every restart.
Success is established rather than assumed. The socket layer bounds the attempt itself, and when the deadline elapses it returns normally, leaving the socket without a peer and its pending error empty. Read as success, that would answer T for a host which never replied and make the deadline branch unreachable, so the socket is asked whether it has a peer before anything goes back. Asking is what catches a path that swallows packets, because such a path leaves no pending error behind at all.
When there is no peer, the pending error decides which failure this was, keyed on the errno in one place. A peer that declined and a path that swallows packets want opposite responses from an operator, a database that is down against a route that is broken, and a probe reporting both after the same wait would send the reader to the wrong place. A refusal arrives on the first round trip and is reported the moment it does, whatever deadline the caller allowed; only a destination that answers nothing at all costs the full DEADLINE.
The host is classified before any of that, and a host this node would have to leave its own network to reach is refused outright rather than probed. This subsystem connects for itself instead of through valis's outbound seam, and the sweep that asserts nothing else does so exempts it on the understanding that the database is the node's own state on the node's own network. A database out in the world is a second unswept way out, so it is refused here and named as something the seam must carry, which keeps the exemption honest rather than merely stated.
Two gaps are accepted rather than overlooked. A host that answers the probe and then stalls inside authentication is not caught: the probe proves reachability, not a completed session. And name resolution runs before the deadline applies, so a wedged resolver is bounded by the resolver's own timeout, not by DEADLINE. Both are far rarer than the unreachable-host cases this catches, and neither is worth a second mechanism here.
operator-state-host-local-p
(operator-state-host-local-p host)
True when HOST names operator state this node reaches without leaving its own network: a local socket path, the loopback name, or an address literal in a loopback, private or link-local range.
Anything else is false, including every name that is not the loopback name. A name is refused rather than resolved, because resolving it here would decide the check on an answer this node would have to go and ask for, and the point of the check is that it holds before anything is asked.
operator-state-pool-armed-p
(operator-state-pool-armed-p)
T when the operator-state pool spec is armed — start-operator-state has run and resolved a DSN into operator-state-spec — NIL otherwise. The single predicate a caller consults to decide whether PG-touching work must run inside with-operator-state-connection: armed means wrap it, unarmed means run it bare so an in-memory / test path with no DSN works without acquiring a connection.
start-operator-state
(start-operator-state &key (max-pool-size *default-max-pool-size*))
Open the bounded operator-state pool after config resolves: bound Postmodern's max-pool-size (it defaults to NIL = unbounded, so the bound is set explicitly) and build the pooled spec from the resolved DSN. Opens NO connection — the pool fills lazily on first use — and must run at runtime, never before the image dump where a live socket would be dead on restart. Returns the pooled spec.
Refuses fail-closed when no DSN resolves (neither operator-state-dsn nor VALISPGDSN): signals operator-state-dsn-unconfigured before touching any global state, rather than arming an unusable all-nil spec a later with-connection would fail on obscurely.
stop-operator-state
(stop-operator-state)
Tear down the operator-state pool: close and empty the Postmodern connection pool and nil the spec special so a subsequent start begins from a known state. Idempotent — a second stop is a no-op — and ignore-errors around the pool clear so a teardown fault does not block the shutdown path. On evacuation the instance is already fenced, so this is cleanup, not the fence.
Macros
with-operator-state-connection
(with-operator-state-connection (&rest options) &body body)
Evaluate BODY with a pooled operator-state connection bound for its dynamic extent (acquire-late / release-early, on a worker thread — never the listener). Expands to postmodern:with-connection over the pooled operator-state-spec; the unwind returns the connection to the bounded pool. OPTIONS is reserved for future connection knobs and is ignored today.
Variables
*default-max-pool-size*
The default ceiling on the bounded Postmodern connection pool. Postmodern's max-pool-size defaults to NIL (unbounded), so the subsystem sets a bound explicitly at start; only the boundedness is locked, the number is tunable.
*reachability-deadline*
Seconds the boot-path reachability probe waits for the operator-state host to answer before declaring it unreachable. Sized against the resident unit's 90s readiness budget: 15s decides early enough to leave the rest of bring-up ample room, and is far short of the kernel's ~130s SYN-retry window, so the probe — not the supervisor's timeout — is always what reports an unreachable database.
Package valis/src/operator-state/renewal-health
Classes
renewal-health
The durable per-cert renewal-health row, keyed by certificate identity. Records LAST-ATTEMPT (absolute universal-time), LAST-ERROR (substrate text, never key material), CONSECUTIVE-FAILURES (the backoff input), NEXT-RETRY (absolute universal-time), and a per-row optimistic GENERATION for the compare-and-swap write guard. Structurally body-never-in-Postgres — no body or header column, a shape the sovereignty audit enforces.
Conditions
stale-renewal-health
Signaled when the renewal-health compare-and-swap matched no row — a lost update against a concurrent legitimate writer (the per-row half of the split-brain guard).
Generic functions
renewal-health-cert-id
(renewal-health-cert-id object)
Undocumented: this exported symbol needs a docstring.
renewal-health-consecutive-failures
(renewal-health-consecutive-failures object)
Undocumented: this exported symbol needs a docstring.
renewal-health-generation
(renewal-health-generation object)
Undocumented: this exported symbol needs a docstring.
renewal-health-last-attempt
(renewal-health-last-attempt object)
Undocumented: this exported symbol needs a docstring.
renewal-health-last-error
(renewal-health-last-error object)
Undocumented: this exported symbol needs a docstring.
renewal-health-next-retry
(renewal-health-next-retry object)
Undocumented: this exported symbol needs a docstring.
Functions
ensure-renewal-health-row
(ensure-renewal-health-row cert-id)
Seed a fresh renewal-health row for CERT-ID when absent — generation 0, zero consecutive failures, no attempt yet — so the first recorded outcome has a row to compare-and-swap. Idempotent (INSERT … ON CONFLICT DO NOTHING). Runs inside the caller's connection/transaction.
record-renewal-outcome
(record-renewal-outcome cert-id expected-generation my-generation &key success error-message next-retry (now (get-universal-time)))
Record a renewal outcome for CERT-ID under the fenced compare-and-swap, the canonical operator-state write composing BOTH guards in ONE transaction.
First assert-fence-touch MY-GENERATION (the live store write-epoch, read per transaction) proves this instance still holds the shared instance fence — a superseded zombie signals fenced-out here and writes nothing, the split-brain stop. A row is seeded when absent so a first outcome has a row to swap. Then the per-row optimistic CAS, gated WHERE generation = EXPECTED-GENERATION, applies the outcome and bumps the generation: on SUCCESS it stamps LAST-ATTEMPT, resets CONSECUTIVE-FAILURES to 0, and clears LAST-ERROR and NEXT-RETRY; on failure it stamps LAST-ATTEMPT, records ERROR-MESSAGE, increments CONSECUTIVE-FAILURES, and schedules NEXT-RETRY. A zero-affected CAS matched no row at EXPECTED-GENERATION — a lost update against a concurrent legitimate writer — and signals stale-renewal-health, writing nothing.
The CAS is an explicit parameterized s-sql :update through postmodern:execute, never a DAO save (which is primary-key based and cannot carry the generation predicate). Returns the affected-row count (1) on success.
renewal-health-for
(renewal-health-for cert-id)
Return the renewal-health dao for CERT-ID, or NIL when no outcome has ever been recorded — the operator/ubik read path. The caller reads GENERATION from the returned row to supply EXPECTED-GENERATION on its next record-renewal-outcome. Runs inside the caller's connection/transaction.
renewal-health-table-ddl
(renewal-health-table-ddl)
Return the CREATE TABLE statement for the renewal-health table, derived from the dao-class slots so the schema and the DAO never drift. The migration runner and the sovereignty audit both consume this string.
Package valis/src/operator-state/runciter-serving-seam
Functions
accept-zone-transfer
(accept-zone-transfer incoming-zone held-serial master now)
Accept or refuse a secondary zone transfer of INCOMING-ZONE (a flat sequence of runciter-typed records incl. the apex SOA) against HELD-SERIAL, attributing it to the opaque upstream MASTER reference at absolute NOW. Delegates to runciter's serving contract. => (values outcome canonical-zone provenance); canonical-zone/provenance non-NIL only on :accepted. Outcome vocabulary is runciter's: :accepted :stale :refused-no-soa :refused-multiple-soa :refused-out-of-set-type :refused-malformed.
advance-refresh-timer
(advance-refresh-timer timer-state event now)
Advance the persisted SOA refresh-timer for EVENT at absolute NOW. Delegates to runciter's serving contract. => (values new-timer-state next-action).
cutover-intent
(cutover-intent cutover-state)
Raw accessor: the operator's unprojected cutover INTENT for the entry — one of :held, :answer-as-secondary, :primary. valis-side identity; runciter has no separate intent symbol (the cutover-state IS the intent).
cutover-transition-legal-p
(cutover-transition-legal-p from to)
Is the cutover transition FROM -> TO legal? Delegates to runciter's serving contract.
effective-status
(effective-status cutover-state timer-state now)
Project the cutover+timer pair to a status at absolute NOW. Delegates to runciter's serving contract.
make-refresh-timer
(make-refresh-timer &rest args)
Construct a persisted crash-safe refresh-timer by delegating to runciter's constructor. Accepts runciter's timer keys, including :soa-refresh :soa-retry :soa-expire :last-successful-refresh-at :expire-deadline :next-wake and :mode.
make-zone-provenance
(make-zone-provenance &key source master serial ingested-at)
Construct a zone-provenance descriptor by delegating to runciter's constructor. Keys: :source :master :serial :ingested-at — the :master is the PUBLIC opaque upstream reference valis injects and runciter echoes, never key bytes.
may-answer-p
(may-answer-p cutover-state timer-state now)
T when the entry may currently answer at absolute NOW. Delegates to runciter's serving contract.
out-of-set-records
(out-of-set-records incoming-zone)
Return the (owner . type) conses of every record in INCOMING-ZONE outside runciter's served set — the offenders that drive a :refused-out-of-set-type outcome, so a caller can name them in an operator/audit surface without re-scanning the zone. Delegates to runciter's serving contract, single-sourced through the same admission predicate accept-zone-transfer uses, so a non-empty result here corresponds exactly to accept-zone-transfer returning :refused-out-of-set-type on the same input. A fully admissible zone yields NIL. Pure; mutates nothing.
refresh-timer-expire-deadline
(refresh-timer-expire-deadline timer)
The absolute expire-deadline of a runciter refresh-timer.
refresh-timer-last-successful-refresh-at
(refresh-timer-last-successful-refresh-at timer)
The absolute last-successful-refresh instant of a runciter refresh-timer.
refresh-timer-next-wake
(refresh-timer-next-wake timer)
The next absolute wake instant recorded on a runciter refresh-timer.
refresh-timer-p
(refresh-timer-p object)
T when OBJECT is a runciter refresh-timer.
runciter-serving-bound-p
(runciter-serving-bound-p name)
T when runciter currently exports an fbound serving symbol named NAME (a string, e.g. "ACCEPT-ZONE-TRANSFER") — i.e. the seam would DELEGATE; NIL when runciter predates that symbol, where the seam FALLS BACK. The single observable predicate a test uses to assert the delegate-vs-fallback decision against a present ("SERIAL<") vs absent symbol.
serial<
(serial< a b)
RFC 1982 serial-number comparison for 32-bit DNS SOA serials.
A is less than B iff A /= B and ((B - A) mod 232) < 231. Inputs are reduced into the 32-bit space first, so callers may pass the opaque version tokens returned by ZONE-VERSION directly. This is the ONLY correct way to order two version tokens; never use CL:< on them.
The exactly-half-way case (the difference is precisely 231) is defined by RFC 1982 as ambiguous. We resolve it consistently as NOT-less-than in both directions, so SERIAL< is antisymmetric on that boundary (neither A<B nor B<A holds when they sit diametrically opposite on the serial circle).
timer-expired-p
(timer-expired-p timer-state now)
Raw accessor: T when the secondary's persisted EXPIRE-DEADLINE has passed at absolute NOW. A NIL timer is never expired; otherwise delegates to runciter's serving contract.
timer-next-wake
(timer-next-wake timer-state)
Raw accessor: the next absolute instant the timer needs attention. A NIL timer has no next wake; otherwise delegates to runciter's serving contract.
version-newer-p
(version-newer-p candidate current)
True when CANDIDATE is a strictly newer zone version than CURRENT under RFC 1982 serial arithmetic. The sanctioned predicate a consumer uses to ask "is my cached view of this zone stale?" — defined entirely in terms of SERIAL< so the two never disagree.
zone-provenance-ingested-at
(zone-provenance-ingested-at provenance)
Read the ingested-at facet of a runciter zone-provenance descriptor.
zone-provenance-master
(zone-provenance-master provenance)
Read the master facet of a runciter zone-provenance descriptor — the PUBLIC opaque upstream reference, never key bytes.
zone-provenance-p
(zone-provenance-p object)
T when OBJECT is a runciter zone-provenance descriptor.
zone-provenance-serial
(zone-provenance-serial provenance)
Read the serial facet of a runciter zone-provenance descriptor.
zone-provenance-source
(zone-provenance-source provenance)
Read the source facet of a runciter zone-provenance descriptor.
Package valis/src/operator-state/runciter-transfer-seam
Conditions
dns-transfer-allowlist-version-mismatch
Signalled by transfer-peer-authorized-p when a caller declares a contract version outside supported-dns-transfer-allowlist-versions. The refusal happens BEFORE any database read — no verdict is computed on a skewed contract, and no partial state is touched.
Generic functions
dns-transfer-allowlist-version-mismatch-declared
(dns-transfer-allowlist-version-mismatch-declared condition)
Undocumented: this exported symbol needs a docstring.
dns-transfer-allowlist-version-mismatch-supported
(dns-transfer-allowlist-version-mismatch-supported condition)
Undocumented: this exported symbol needs a docstring.
Functions
serial<
(serial< a b)
RFC 1982 serial-number comparison for 32-bit DNS SOA serials.
A is less than B iff A /= B and ((B - A) mod 232) < 231. Inputs are reduced into the 32-bit space first, so callers may pass the opaque version tokens returned by ZONE-VERSION directly. This is the ONLY correct way to order two version tokens; never use CL:< on them.
The exactly-half-way case (the difference is precisely 231) is defined by RFC 1982 as ambiguous. We resolve it consistently as NOT-less-than in both directions, so SERIAL< is antisymmetric on that boundary (neither A<B nor B<A holds when they sit diametrically opposite on the serial circle).
transfer-peer-authorized-p
(transfer-peer-authorized-p zone-id peer-ref key-name &key (contract-version +dns-transfer-allowlist-contract-version+))
The versioned, fail-closed transfer-authorization verdict: T only when ZONE-ID authorizes PEER-REF presenting KEY-NAME (the three-way allowlist conjunction), NIL otherwise. Runs the contract-version check FIRST: a CONTRACT-VERSION outside supported-dns-transfer-allowlist-versions signals DNS-TRANSFER-ALLOWLIST-VERSION- MISMATCH BEFORE any database read — refuse-before-act, so a skewed contract yields a signal, never a boolean verdict. Fail-closed on every other uncertainty too: an unmatched allowlist request denies rather than authorizing. The conjunction delegate ships in this same system and is bound in-image by compile-time import, so DENY is a computed verdict, not an absent-delegate fallback. KEY-NAME is a PUBLIC reference — the seam names the key, never the bytes.
A denied verdict is logged once as a transfer.refused substrate event naming the zone, the peer, and the public key name, so a refused pull surfaces as a discriminable event rather than only the secondary's opaque transport error. The secret is never a field — the key NAME is public, the bytes are the credential authority's.
transfer-seam-delegate-bound-p
(transfer-seam-delegate-bound-p)
T when the conjunction delegate is fbound — the seam will DELEGATE the verdict; NIL when it is absent, where the seam FALLS CLOSED to DENY. The single observable predicate a test uses to assert the delegate-vs-deny decision.
version-newer-p
(version-newer-p candidate current)
True when CANDIDATE is a strictly newer zone version than CURRENT under RFC 1982 serial arithmetic. The sanctioned predicate a consumer uses to ask "is my cached view of this zone stale?" — defined entirely in terms of SERIAL< so the two never disagree.
Variables
*supported-dns-transfer-allowlist-versions*
The contract versions transfer-peer-authorized-p admits. A caller declaring a version outside this set is refused fail-closed — no silent skew with the transfer service.
Constants
+dns-transfer-allowlist-contract-version+
The transfer-allowlist read-seam contract version valis currently exposes. With the authorization read and the supported set, this is the surface valis signals the out-of-tree transfer service (runciter) — the one coupling across the repo boundary.
Package valis/src/operator-state/secondary
Conditions
cannot-redeclare-primary-as-secondary
Signaled by declare-secondary-zone when the zone already exists as an operator-authored PRIMARY (authoritystate=primary). Declaring it as a secondary would downgrade valis's own system-of-record zone into a tracked cache of an upstream master; refused fail-closed, the declaration commits nothing.
no-secondary-anchor
Signaled by persist-refresh-timer-state when the ndbentry anchor for the zone is absent: the fenced UPDATE would touch zero rows and silently drop the crash-safe deadline. Refused loudly so a lost-timer bug surfaces instead of a stale secondary answering past an expiry that was never recorded.
secondary-transfer-refused
Signaled by ingest-secondary-zone when runciter's admission declines the transfer (a non-:accepted outcome). OUTCOME is the seam's verdict keyword; on :refused-out-of-set-type, OFFENDING-RECORDS names the (owner . type) of each out-of-set record, single-sourced through the seam's out-of-set-records helper so the audit surface names the offender without valis re-scanning the zone.
upstream-master-not-allowed
Signaled by ingest-secondary-zone when the upstream master is not on the operator-curated upstreammasterallow allowlist for the zone. Raised BEFORE the fence and before any write, so the refusal commits nothing (fail-closed).
Generic functions
cannot-redeclare-primary-as-secondary-origin
(cannot-redeclare-primary-as-secondary-origin condition)
Undocumented: this exported symbol needs a docstring.
no-secondary-anchor-origin
(no-secondary-anchor-origin condition)
Undocumented: this exported symbol needs a docstring.
secondary-transfer-refused-offending-records
(secondary-transfer-refused-offending-records condition)
Undocumented: this exported symbol needs a docstring.
secondary-transfer-refused-outcome
(secondary-transfer-refused-outcome condition)
Undocumented: this exported symbol needs a docstring.
upstream-master-not-allowed-master-ref
(upstream-master-not-allowed-master-ref condition)
Undocumented: this exported symbol needs a docstring.
upstream-master-not-allowed-zone-id
(upstream-master-not-allowed-zone-id condition)
Undocumented: this exported symbol needs a docstring.
Functions
add-secondary-peer
(add-secondary-peer zone-id peer-ref)
Add PEER-REF to ZONE-ID's downstream secondary-peer feed allowlist (idempotent).
allow-upstream-master
(allow-upstream-master zone-id master-ref)
Add MASTER-REF to ZONE-ID's upstream-master acceptance allowlist (idempotent). A PUBLIC master reference only — never key bytes.
declare-secondary-zone
(declare-secondary-zone origin upstream-master my-generation &key owner-did)
Operator declaration that bootstraps a brand-new HELD secondary BEFORE any transfer, resolving the upstreammasterallow -> dnszone(origin) foreign-key ordering: a master cannot be allowlisted before the zone row exists, yet the ingest door refuses an un-allowlisted master, so a cold secondary's FIRST transfer could never self-authorize. Declaration is the operator gate that breaks the deadlock — in ONE fenced transaction and in FK order it creates a held dnszone SHELL (a placeholder apex the first transfer overwrites), its ndbentry anchor (authoritystate=secondary, provenancekind=secondary, cutoverstate=held, upstreammaster), and the upstreammasterallow entry. The first allowlisted transfer then populates the real records through ingest-secondary-zone.
OWNER-DID is the controlling operator stamped on the shell's ownerdid. Idempotent: re-declaring an existing zone leaves its records intact and only ensures the held anchor + the allowlist. A PUBLIC master reference only, never key bytes. Assert the instance fence FIRST (a superseded instance declares nothing). Runs inside the caller's pooled operator-state connection. Returns the canonical origin.
ingest-secondary-zone
(ingest-secondary-zone decoded-zone upstream-master my-generation now &key owner-did)
Ingest DECODED-ZONE (a flat sequence of runciter-typed records incl. the apex SOA, handed in-process by the AXFR-in engine) as a HELD secondary attributed to the opaque public UPSTREAM-MASTER reference at absolute NOW. MY-GENERATION is the live store write-epoch this instance holds. OWNER-DID, when supplied, is the controlling operator (ownerdid) for a freshly-declared secondary; on a refresh of an existing secondary it defaults to the stored owner — control is unchanged by authorship.
The door authorizes FIRST on the operator-curated upstream-master allowlist, fail-closed, BEFORE the fence and before any write: an un-allowlisted master signals upstream-master-not-allowed and commits nothing. An allowlisted transfer is run through the valis-owned serving seam's accept-zone-transfer for the DNS-semantic admission decision (served-type set, SOA presence); a non-:accepted verdict signals secondary-transfer-refused — naming the out-of-set offenders via the seam on the :refused-out-of-set-type path, never re-scanning the zone.
An accepted transfer commits through commit-zone-records with a :secondary provenance and :cutover-state :held, so resolve-serial (the authoritative RFC-1982 monotonicity gate — a stale serial is refused transactionally as zone-serial-regression), the fence (a superseded instance writes nothing), the full-zone replace and the ndbentry stamping all run in that one transaction. The committed zone is recorded with cutover-state :held, so it is tracked but not made answerable: only an operator cutover-state flip makes it eligible, at which point the serving authority loads and answers it. Returns the resolved serial.
list-secondary-peers
(list-secondary-peers zone-id)
The public peer references ZONE-ID is fed to as downstream secondaries.
list-upstream-masters
(list-upstream-masters zone-id)
The public master references allowlisted to feed ZONE-ID as a secondary.
persist-refresh-timer-state
(persist-refresh-timer-state origin new-timer-state my-generation &key source-serial)
Persist runciter advance-refresh-timer's NEW-TIMER-STATE for the secondary at ORIGIN through a fenced ndbentry UPDATE: the absolute lastrefreshat / expiredeadline / retrydeadline, derived from durable instants against an injected `now`, never process uptime — so a restart cannot extend a secondary's expiry window. SOURCE-SERIAL, when supplied, records the last-accepted upstream serial alongside.
Assert the instance fence FIRST (a superseded instance persists nothing — the split-brain stop). Runs inside the caller's pooled operator-state connection.
remove-secondary-peer
(remove-secondary-peer zone-id peer-ref)
Remove PEER-REF from ZONE-ID's downstream secondary-peer feed allowlist.
revoke-upstream-master
(revoke-upstream-master zone-id master-ref)
Remove MASTER-REF from ZONE-ID's upstream-master acceptance allowlist.
secondary-peer-allowed-p
(secondary-peer-allowed-p zone-id peer-ref)
T when PEER-REF is on ZONE-ID's downstream secondary-peer feed allowlist.
upstream-master-allowed-p
(upstream-master-allowed-p zone-id master-ref)
T when MASTER-REF is on the operator-curated upstream-master acceptance allowlist for ZONE-ID — the authority the secondary door gates on, never the session principal.
Package valis/src/operator-state/secondary-enrollment
Functions
default-transfer-key-name
(default-transfer-key-name origin)
Derive a TSIG key name for ORIGIN when the operator supplies none: "xfr-" then ORIGIN with any single trailing root dot dropped (e.g. "example.com." -> "xfr-example.com"). The result is a valid DNS name, the form the allowlist and mercer both canonicalise, and reads at a glance as this zone's transfer key.
generate-tsig-secret
(generate-tsig-secret)
A fresh HMAC-SHA256 shared secret: 32 octets from the OS CSPRNG. Returned as a simple octet vector for mercer's custody and for base64 rendering to the operator.
provision-tsig-key
(provision-tsig-key key-name &optional (algorithm +tsig-algorithm-dns-name+))
Mint a fresh TSIG secret, persist it into mercer's DURABLE custody under KEY-NAME and ALGORITHM, and register it into this process's in-image store. Returns (values canonical-key-name secret-base64): the canonical name mercer keyed the secret under, and the secret base64-encoded for the operator to install on the secondary. The raw secret is not returned in any other form.
The load-bearing step is the durable persist: it writes a 0600 file under the resident's state root, and mercer's read-through key-loader consults that file on an in-image miss. So a key minted by a short-lived enrollment process is authenticated by the RUNNING resident on the secondary's next TSIG-signed AXFR — no resident restart, no reload. The in-image register is kept because its return is the canonical key name; it is a no-op for the resident's benefit in the CLI path (that process exits), but keeps provisioning identical whether run in the resident or a separate enrollment process.
record-transfer-authorization
(record-transfer-authorization origin peer key-name &key (notify-ref peer))
Record the two operator-state facts that let PEER pull ORIGIN: the transfer allowlist row over (ORIGIN, PEER, KEY-NAME), and the also-notify recipient NOTIFY-REF (defaulting to PEER, so the peer that pulls is also told when to). Idempotent — a re-run of an identical enrollment is a no-op. ORIGIN is canonicalised by the primitives to the stored zone key. Must run inside an operator-state connection; the caller owns the pool and the transaction. Stores only PUBLIC references — a peer address and a key NAME — never secret bytes.
render-bind-snippet
(render-bind-snippet key-name secret-base64 &key (algorithm +tsig-algorithm-bind-name+) (master-address (require-transfer-master-address)))
A ready-to-paste BIND fragment for the secondary: the key{} clause carrying the shared secret, and a comment showing the masters{} reference that names the key on the zone's transfer. KEY-NAME is quoted exactly as the operator will reference it; the secret is the base64 mercer's custody holds the bytes of.
MASTER-ADDRESS is the address the secondary pulls from, defaulting to this node's configured authoritative-DNS address. With none supplied and none configured the render refuses: a masters{} clause naming an address nobody chose is a fragment the operator pastes and only finds wrong when transfers fail.
Variables
+tsig-algorithm-bind-name+
The same algorithm as BIND names it in a key{} clause (no trailing dot).
+tsig-algorithm-dns-name+
The TSIG algorithm as a DNS name, the form mercer's key custody registers under (RFC 8945 §6). mercer originates and accepts only HMAC-SHA256; a held parameter, not a defconstant, because a string is not eql-redefinable on reload.
Package valis/src/operator-state/served-names
Functions
resolve-served-names
(resolve-served-names custody-domains)
Return (values NAMES STATUS) — the DNS names this node serves on the public :443, and how the owned-zone half of the answer was reached.
NAMES is the declared primary names, followed by those CUSTODY-DOMAINS this instance also answers the DNS for. The second half is an INTERSECTION and that is the whole point: custody on its own admits nothing, so material placed in the store by any route other than the operator's own declaration or a zone they control cannot cause a name to be served. Duplicates across the two collapse; names differing only in case or a trailing dot are one name. Order is stable, and the base name — the certificate presented when no choice can be made — stays first whenever it is configured.
Each name is returned in the spelling it arrived in, since that is the spelling the store and the wire use; only the comparison is canonicalised.
STATUS passes through from the owned-zone read: :QUERIED when operator state answered, :OPERATOR-STATE-UNREACHABLE when it could not be read. A caller that logs an empty result needs it, because "no zone qualifies" and "the database was not reachable" produce the same empty list and mean opposite things.
CUSTODY-DOMAINS may be empty, and this verb reads no store: the caller supplies what custody holds.
Package valis/src/operator-state/social-edge
Classes
social-edge
A single directed, typed edge of the operator's social graph: edge(srcdid, dstdid, reltype, generation, createdat), keyed by (src-did, dst-did, rel-type). rel-type discriminates follow / block / contact-class (extensible). There is NO node table — the DID endpoints are owned by the identity axis — and NO body pointer — an edge holds no content; every column is public operator state. A per-row optimistic GENERATION guards lost updates; mutations ride the instance fence.
Conditions
stale-edge
Signaled when a per-row edge compare-and-swap matched no row — a lost update against a concurrent legitimate writer (or an edge whose generation no longer matches the mutation's precondition). The transaction writes nothing.
Generic functions
edge-created-at
(edge-created-at object)
Undocumented: this exported symbol needs a docstring.
edge-dst-did
(edge-dst-did object)
Undocumented: this exported symbol needs a docstring.
edge-generation
(edge-generation object)
Undocumented: this exported symbol needs a docstring.
edge-rel-type
(edge-rel-type object)
Undocumented: this exported symbol needs a docstring.
edge-src-did
(edge-src-did object)
Undocumented: this exported symbol needs a docstring.
Functions
add-edge
(add-edge src-did dst-did rel-type my-generation &key (created-at 0))
Add the directed edge (SRC-DID -REL-TYPE-> DST-DID), idempotently: a re-add of an existing edge collapses onto the existing row and never creates a duplicate (:on-conflict-do-nothing on the composite key). Asserts the instance fence at MY-GENERATION first (a superseded writer writes nothing), then inserts. Returns the affected-row count — 1 on a fresh edge, 0 on an idempotent re-add.
remove-edge
(remove-edge src-did dst-did rel-type expected-generation my-generation)
Remove the directed edge (SRC-DID -REL-TYPE-> DST-DID) iff its generation still equals EXPECTED-GENERATION — a per-row compare-and-swap expressed as a predicated :delete-from. Asserts the instance fence at MY-GENERATION first (a superseded writer writes nothing). Returns 1 affected on success; signals stale-edge and writes nothing when no row matches the edge-and-generation predicate (a lost update or an already-removed edge).
social-edge-table-ddl
(social-edge-table-ddl)
Return the CREATE TABLE statement for the socialedge table, derived from the dao-class slots so the schema and the DAO never drift. The migration runner and the sovereignty audit both consume this string.
traverse-reachable
(traverse-reachable start-did &key (rel-type "follow") (max-depth 64))
Return the list of DIDs reachable from START-DID along edges of REL-TYPE, as a bounded recursive-CTE traversal of the adjacency graph. Terminates on a cyclic graph: the recursion is bounded by MAX-DEPTH, and the final select returns each reachable DID exactly once (UNION discards revisited rows; the depth bound breaks follow/block cycles — Postmodern's documented cycle guidance). Runs inside the caller's connection/transaction.
UNAUTHENTICATED: this primitive performs NO capability check. It is reachable only through the /social gate (src/plugin/social-admission:query-reachable), the single caller-facing entry; calling it directly bypasses the /social :read authorization.
Package valis/src/operator-state/transfer-allowlist
Functions
address-literal-p
(address-literal-p reference)
T when REFERENCE is an IPv4 or IPv6 address literal rather than a DNS name.
allow-also-notify
(allow-also-notify zone-id notify-ref)
Add NOTIFY-REF to ZONE-ID's also-notify recipient set (idempotent). ZONE-ID is canonicalised at write. NOTIFY-REF is normalised by its shape: an IPv4 or IPv6 address literal is stored as given with no trailing dot, a DNS name takes the same canonicalisation as every other name column here. A PUBLIC recipient reference only, never key bytes.
allow-transfer-peer
(allow-transfer-peer zone-id peer-ref key-name)
Authorize PEER-REF, presenting KEY-NAME, to transfer ZONE-ID (idempotent — a second identical call is a no-op via on-conflict-do-nothing). ZONE-ID and KEY-NAME are canonicalised and PEER-REF is case-folded at write — the same normalisation the read applies, so a curated mixed-case entry is found by the canonical serving-side lookup. A PUBLIC peer reference and key NAME only, never key bytes.
also-notify-listed-p
(also-notify-listed-p zone-id notify-ref)
T when NOTIFY-REF is on ZONE-ID's also-notify recipient set. ZONE-ID is canonicalised to the stored zone key and NOTIFY-REF takes the same shape-dependent normalisation the write applies, so a lookup keys the row a write stored, for an address literal and a DNS name alike. Distinct from the transfer allowlist: a NOTIFY recipient is not automatically transfer-authorized.
list-also-notify
(list-also-notify zone-id)
The additional-NOTIFY recipient references for ZONE-ID as stored. ZONE-ID is canonicalised to the stored zone key.
list-transfer-peers
(list-transfer-peers zone-id)
The (peer-ref . key-name) authorizations for ZONE-ID as stored — the rows a serving-side authorization check reads. ZONE-ID is canonicalised to the stored key.
notify-ref-for-dispatch
(notify-ref-for-dispatch notify-ref)
NOTIFY-REF as a dispatcher can use it: an address loses a trailing dot, a name does not.
revoke-also-notify
(revoke-also-notify zone-id notify-ref)
Remove NOTIFY-REF from ZONE-ID's also-notify recipient set. ZONE-ID is canonicalised so a relative and an absolute origin address the same row.
revoke-transfer-peer
(revoke-transfer-peer zone-id peer-ref key-name)
Revoke the (ZONE-ID, PEER-REF, KEY-NAME) transfer authorization — removes exactly that row. ZONE-ID is canonicalised so a relative and an absolute origin address the same row.
transfer-peer-allowed-p
(transfer-peer-allowed-p zone-id peer-ref key-name)
T only when ZONE-ID authorizes PEER-REF presenting KEY-NAME — a single row must match all three columns (the conjunction IS the authorization). A right-key/wrong-peer and a wrong-key/right-peer both return NIL; absence from the allowlist IS refusal. ZONE-ID and KEY-NAME are canonicalised to their stored DNS form and PEER-REF is case-folded before the compare, so a mixed-case entry matches the canonical form a serving-side check presents — all three case-insensitive DNS names.
Package valis/src/operator-state/zone-feed
Classes
zone-change-listener-spec
One registered zone-change listener. NAME is the listener's name (the registry key material). HANDLER is a function of (origin serial) the wire engine binds to react to a committed zone change. CONTRACT-VERSION is the feed contract version the binding engine declared at registration — the value the register-time fail-closed check admitted.
Conditions
zone-feed-version-mismatch
Signalled by register-zone-change-listener when a binding engine declares a contract version outside supported-zone-feed-contract-versions. The registry is left unchanged — the refusal happens before any setf gethash.
Functions
add-secondary-peer
(add-secondary-peer zone-id peer-ref)
Add PEER-REF to ZONE-ID's downstream secondary-peer feed allowlist (idempotent).
fire-zone-change
(fire-zone-change origin serial)
Invoke every registered zone-change listener with (ORIGIN SERIAL), in the deterministic STRING<-sorted order, signalling that the zone at ORIGIN committed at SERIAL. Each listener runs under its OWN handler-case: a listener that signals is warned and SKIPPED, the remaining listeners still fire, and the error never unwinds out of fire-zone-change. This matters because fire-zone-change runs off the shared post-commit hook — an out-of-tree listener (c3po) throwing must not turn a durably committed transfer into an apparent failure, nor abort its sibling listeners. This is the data-side signal only; whether a listener actually emits a wire NOTIFY for ORIGIN (gating on cutover-state and the downstream peer allowlist) is the wire engine's concern. Returns the number of listeners invoked.
notify-recipient-set
(notify-recipient-set origin &optional (source (make-instance (quote pg-zone-source))))
The NOTIFY recipients for the zone at ORIGIN: the apex NS nameserver names (the NS-record targets owned by the canonical apex) UNIONed with the operator-curated also-notify references. A stored recipient that is an address literal is returned without any FQDN trailing dot, whatever notation the row was written in, so a recipient recorded before the write path told addresses from names is still dispatchable. Returns NAMES only, never resolved A/AAAA; resolving a nameserver name to an address is the transfer service's, not valis's. The set is DISTINCT from the transfer allowlist and never merged with it. ORIGIN is canonicalised the same way zone-feed-peers keys its allowlist. SOURCE defaults to the PG read surface; a test may inject any zone-data-source.
register-zone-change-listener
(register-zone-change-listener &key name handler (contract-version +zone-feed-contract-version+))
Register a zone-change listener under NAME with HANDLER declaring CONTRACT-VERSION. Runs the fail-closed version check FIRST: if CONTRACT-VERSION is not in supported-zone-feed-contract-versions it signals ZONE-FEED-VERSION-MISMATCH and adds NO entry — the refuse-before-act discipline leaves no partial registry state. Otherwise builds a fresh ZONE-CHANGE-LISTENER-SPEC and stores it under (STRING NAME). IDEMPOTENT: re-registering the same name REPLACES the spec with no error. HANDLER is a function of (origin serial) invoked by fire-zone-change. Returns the spec.
remove-secondary-peer
(remove-secondary-peer zone-id peer-ref)
Remove PEER-REF from ZONE-ID's downstream secondary-peer feed allowlist.
secondary-peer-allowed-p
(secondary-peer-allowed-p zone-id peer-ref)
T when PEER-REF is on ZONE-ID's downstream secondary-peer feed allowlist.
signal-zone-advance
(signal-zone-advance origin serial)
The strict-advance NOTIFY gate installed on zone-committed-hook. Emits the zone-change signal (fire-zone-change) for ORIGIN at SERIAL only when SERIAL strictly advances the last serial signalled for ORIGIN under RFC 1982 (version-newer-p, re-exported through the serving seam — never CL:<). A serial equal to or older than the last signalled one commits durably but emits NO NOTIFY, so a no-op re-commit or a rollback attempt never announces a spurious change to peers. The check-and-set on the shared serial table is done under last-signaled-serials-lock so concurrent commits cannot race it; the listener fan-out runs OUTSIDE the lock. Returns the number of listeners fired on a strict advance, or NIL when the signal is suppressed.
unregister-zone-change-listener
(unregister-zone-change-listener name)
Remove the listener registered under NAME (normalized via STRING). Returns T if a listener was present and removed, NIL otherwise.
zone-change-listener-spec-contract-version
(zone-change-listener-spec-contract-version instance)
Undocumented: this exported symbol needs a docstring.
zone-change-listener-spec-handler
(zone-change-listener-spec-handler instance)
Undocumented: this exported symbol needs a docstring.
zone-change-listener-spec-name
(zone-change-listener-spec-name instance)
Undocumented: this exported symbol needs a docstring.
zone-change-listeners
(zone-change-listeners)
Return the registered ZONE-CHANGE-LISTENER-SPECs in a deterministic order, sorted by name via STRING<. The stable order is what gives fire-zone-change a stable iteration order across runs.
zone-feed-peers
(zone-feed-peers origin)
The downstream secondary peers ORIGIN may be fed AXFR-out to — the operator-curated allowlist, read through operator-state. Only allowlisted peers are returned; a peer absent from secondarypeer is excluded, so the wire feed never feeds an un-allowlisted peer (the data gate; the wire enforcement is c3po-dns's). ORIGIN is canonicalised to the stored zone key. Curation (add/remove) ships in the secondary door and is re-exported here.
zone-feed-serial
(zone-feed-serial origin &optional (source (make-instance (quote pg-zone-source))))
The feed's current serial for the zone at ORIGIN — the AXFR-out version token, RFC-1982-monotonic and never lowered — by delegating to the shipped pg-zone-source zone-version read. No new query code. SOURCE defaults to the PG read surface; a test may inject any zone-data-source. NIL when no such zone is stored.
zone-feed-snapshot
(zone-feed-snapshot origin &optional (source (make-instance (quote pg-zone-source))))
The feed's full-zone snapshot for the zone at ORIGIN — the complete AXFR-out record set as runciter resource-record structs — by delegating to the shipped pg-zone-source zone-record-set read. No new query code. NIL when no such zone is stored.
Variables
*last-signaled-serials*
Canonical origin -> the last serial for which a NOTIFY-relevant change signal was emitted. signal-zone-advance consults and updates this so a serial that does not strictly advance emits no wire NOTIFY. Process-resident: a fresh image re-signals on the first commit per origin, which is safe — a spurious NOTIFY only prompts a peer to re-check the SOA, never rolls a zone back.
*supported-zone-feed-contract-versions*
The contract versions register-zone-change-listener admits. A binding engine declaring a version outside this set is refused fail-closed — no silent skew with the wire engine.
*zone-change-listeners*
Listener name -> ZONE-CHANGE-LISTENER-SPEC. The key is the name normalized via STRING, so :notify, "notify" and 'notify collapse to one key. The out-of-tree c3po-dns AXFR-out / NOTIFY engine installs onto this registry; valis names no concrete listener. fire-zone-change iterates it to signal a committed zone change.
Constants
+zone-feed-contract-version+
The outbound-feed seam contract version valis currently exposes. With the registry and register-zone-change-listener, this is the surface valis signals the out-of-tree wire engine over the bus.
Package valis/src/operator-state/zone-view
Variables
*zone-view-clock*
A thunk returning the current absolute time as UNIX epoch-seconds — the same numeric domain SOA refresh timers compute in, and the domain a secondary's persisted expire deadline is stored in. The cutover legality gate funcalls it to enforce a secondary's absolute expiry at serve time. Rebindable so a test drives expiry deterministically against an injected now.
Package valis/src/operator-state/zones
Classes
dns-record
One resource-record-set member as an operator-state dao-class, keyed by (zone-id, owner, record-type, rdata-digest) within its zone. The unbounded canonical rdata presentation text is never indexed; the no-duplicate-rrset-member invariant rides the bounded fixed-width rdata-digest instead, keeping the dedup key under PostgreSQL's btree limit. DNS has no content boundary — every column is published metadata.
dns-zone
The DNS zone apex as an operator-state dao-class, keyed on its canonical origin: the SOA authority fields (mname, rname, refresh/retry/expire/minimum), the wire-visible serial, the owner DID that holds write authority, and an optimistic generation. Every column is published-by-design metadata — nothing secret enters :names. The origin doubles as the ndbentry name for kind "dns-zone".
pg-zone-source
A zone-data-source backed by the dnszone / dnsrecord rows on the operator-state PG seam — the published-by-design, world-readable read surface for the :names axis. Methods query the ambient postmodern connection, so a caller wraps a read in with-operator-state-connection. The read methods return runciter resource-record structs: valis builds each envelope from its typed PG columns and crosses only the rdata text to runciter:parse-rdata.
Conditions
cname-conflict
Signaled by the transient-member seam's CNAME-exclusivity guard: a CNAME cannot coexist with any other record at a name, and no name may hold a second record alongside a CNAME. The one per-type invariant a single-member add can violate.
duplicate-rrset-member
Two records share the same (owner, type, rdata-digest) — a duplicate rrset member, the application-level mirror of the composite key.
durable-member-not-found
Signaled by replace-durable-rrset-member when the member being replaced has no durable row at its composite key (zoneid, owner, recordtype, rdatadigest). A replace targets an existing durable member; with none present the operation is not a replace, so it is refused rather than silently degrading to a bare add — the caller learns the old member it named does not exist.
missing-apex-ns
The zone has no NS record at its apex origin.
missing-apex-soa
The zone has no single SOA at its apex origin.
name-outside-zone
A record owner is not at or under the zone origin.
transient-durable-collision
Signaled by publish-transient-rrset-member when the composite PK (zoneid, owner, recordtype, rdatadigest) of the member being published already belongs to a DURABLE row. Publishing errors rather than shadowing or later deleting the operator's durable record — the structural retract-safety guarantee. It never inserts, deletes, or overwrites the durable record.
transient-soa-refused
Signaled by publish-transient-rrset-member when a caller tries to publish a transient SOA. A zone has a single SOA, a singleton at the apex; seeding a transient SOA would place a second SOA there, which a resolver would answer nondeterministically. Refused before any rdata parse or database work — nothing is inserted and the serial never bumps. NS, a legitimate multi-member rrset, is not refused.
zone-include-refused
Signaled when operator-submitted master text uses $INCLUDE. The default parser reader would read arbitrary host filesystem paths, so the import path passes a closure that refuses $INCLUDE outright.
zone-invariant-violation
Base condition for a parsed zone that violates a relational / structural invariant valis owns. runciter has already proven each RR is syntactically valid and canonical; this layer is valis's relational guarantee.
zone-not-answerable
Signaled by the transient-member seam's answerability guard when the target zone is not at cutover-state :primary (it is :held, answer-as-secondary, or absent). A transient single-member add is refused into a zone valis does not answer as primary — never seed a member into a non-answering zone.
zone-serial-regression
Signaled by resolve-serial when an incoming SOA serial is strictly older than the stored one (RFC 1982). A regression is refused so the served view can never be rolled back or poisoned.
Generic functions
durable-member-not-found-owner
(durable-member-not-found-owner condition)
Undocumented: this exported symbol needs a docstring.
durable-member-not-found-rdata-digest
(durable-member-not-found-rdata-digest condition)
Undocumented: this exported symbol needs a docstring.
durable-member-not-found-record-type
(durable-member-not-found-record-type condition)
Undocumented: this exported symbol needs a docstring.
record-class
(record-class object)
Undocumented: this exported symbol needs a docstring.
record-owner
(record-owner object)
Undocumented: this exported symbol needs a docstring.
record-rdata
(record-rdata object)
Undocumented: this exported symbol needs a docstring.
record-rdata-digest
(record-rdata-digest object)
Undocumented: this exported symbol needs a docstring.
record-transient
(record-transient object)
Undocumented: this exported symbol needs a docstring.
record-ttl
(record-ttl object)
Undocumented: this exported symbol needs a docstring.
record-type
(record-type object)
Undocumented: this exported symbol needs a docstring.
record-zone-id
(record-zone-id object)
Undocumented: this exported symbol needs a docstring.
zone-expire
(zone-expire object)
Undocumented: this exported symbol needs a docstring.
zone-generation
(zone-generation object)
Undocumented: this exported symbol needs a docstring.
zone-minimum
(zone-minimum object)
Undocumented: this exported symbol needs a docstring.
zone-mname
(zone-mname object)
Undocumented: this exported symbol needs a docstring.
zone-origin
(zone-origin object)
Undocumented: this exported symbol needs a docstring.
zone-owner-did
(zone-owner-did object)
Undocumented: this exported symbol needs a docstring.
zone-refresh
(zone-refresh object)
Undocumented: this exported symbol needs a docstring.
zone-retry
(zone-retry object)
Undocumented: this exported symbol needs a docstring.
zone-rname
(zone-rname object)
Undocumented: this exported symbol needs a docstring.
zone-serial
(zone-serial object)
Undocumented: this exported symbol needs a docstring.
Functions
add-durable-rrset-member
(add-durable-rrset-member origin owner type ttl rdata my-generation &key owner-did)
Add ONE DURABLE rrset member (OWNER TYPE TTL RDATA) to the :primary zone at ORIGIN, held under the store write-epoch MY-GENERATION — the durable generalization of publish-transient-rrset-member. TYPE is a runciter RR-type keyword; RDATA is its presentation text, crossed to runciter:parse-rdata (the sole RR-syntax authority) and rendered canonical so the stored columns are byte-identical to the import path. OWNER-DID, captured from the session principal at the :names write gate, is stamped as the zone's ownerdid.
One fence-guarded transaction that asserts the instance fence FIRST (the split-brain stop), runs the answerability guard, then resolves the member against its composite key (zoneid, owner, recordtype, rdatadigest):
- an identical DURABLE row already present → idempotent no-op, NO serial bump;
- a TRANSIENT row at the key → promoted to durable (the owner now durably asserts the member) with its TTL updated, a real change → one serial bump;
- absent → the CNAME-exclusivity guard runs, one durable row inserts, the FULL invariant set is re-asserted over the resulting served set (a second apex SOA is caught here and rolled back), and the serial advances once.
On a real change the zone-committed-hook fires OUTSIDE the transaction with (canonical-origin resolved-serial) so the outbound feed reaches secondaries; a no-op neither bumps nor fires. Runs inside the caller's pooled operator-state connection. Returns (values RESOLVED-SERIAL CHANGED-P UNSERVED-REASON), where UNSERVED-REASON is NIL when the edit also reached the serving side and otherwise names why it did not, so a caller can report a durable-but-unpropagated edit as the distinct outcome it is.
apex-ns-present
(apex-ns-present records origin)
Signal missing-apex-ns unless at least one NS record is owned by the canonical apex ORIGIN.
commit-zone-records
(commit-zone-records records origin my-generation provenance &key (cutover-state :primary) in-transaction-hook)
Commit RECORDS (a flat sequence of runciter-typed resource records including the apex SOA) as the atomic full-zone replacement of the zone at ORIGIN, stamping the ndb-general anchor from PROVENANCE. The shared inner door BOTH the text path (import-zone-master-text) and the secondary path (ingest-secondary-zone) commit through, so wire-ingested and text-ingested zones converge on one persistence body.
ORIGIN is canonicalised defensively (idempotent on an absolute, dotted FQDN). MY-GENERATION is the live store write-epoch this instance holds. PROVENANCE is the authorship descriptor: ownerdid derives from its PRINCIPAL (control, unchanged by authorship), while authoritystate / provenancekind / upstreammaster record WHO AUTHORED the data. CUTOVER-STATE is the lifecycle state this commit lands at (:primary for an operator zone, :held for a freshly-ingested secondary).
The structural invariants run BEFORE any database work, so a malformed or invariant-violating zone never touches stored rows. The write then opens one logical transaction, asserts the instance fence FIRST (a superseded instance signals fenced-out and writes nothing — the split-brain stop), reads the stored serial and resolves the new one under RFC 1982 monotonicity (a regression signals and rolls the transaction back, never lowered), deletes the zone's existing DURABLE records — a live transient member (a published challenge) is spared and survives the re-import, with a warning so the operator knows one is live — upserts the apex/SOA zone row with the resolved serial and a bumped generation, stamps the ndbentry facets in the SAME transaction, and re-inserts every record with its bounded rdata digest. The whole replace commits or rolls back as one unit.
IN-TRANSACTION-HOOK, when supplied, is a function of (canonical-origin resolved-serial) called INSIDE that same transaction once every record is in place. It is how a caller joins its own operator-state writes to this zone's fate: both land or neither does, and a hook that signals rolls the zone back with it. Deliberately distinct from zone-committed-hook, which fires AFTER the commit and must stay that way: a listener there observes a durable change and cannot roll it back, and moving transactional work onto it would take that property away. A hook is for a write that belongs to the same operator act; it is not a notification seam.
After the transaction commits durably, the inverted zone-committed-hook fires with (canonical-origin resolved-serial) so the outbound-feed change signal emits off this one shared commit point for BOTH ingest paths; it fires OUTSIDE the transaction, on success only, so a listener observes a durable change and cannot roll the commit back.
This door never makes the zone answerable: a :held secondary is tracked but does not answer until the operator flips its cutover-state, at which point the serving authority loads and answers it. The answerability gate is the serving authority's, not this door's.
A re-import assumes a single writer: if the freshly parsed master carries a durable record whose composite key collides with a surviving transient member, the re-insert raises a primary-key violation and the whole replace rolls back. This door does not detect-and-republish that case — resolving it belongs to the challenge manager that owns the publish/poll/retract lifecycle.
Returns (values RESOLVED-SERIAL UNSERVED-REASON): the serial the zone now carries, and NIL when the durable change also reached the serving side or a keyword from the closed set naming why it did not.
delete-durable-rrset-member
(delete-durable-rrset-member origin owner type rdata my-generation &key owner-did)
Delete ONE DURABLE rrset member (OWNER TYPE RDATA) from the :primary zone at ORIGIN, held under the store write-epoch MY-GENERATION — the durable generalization of retract-transient-rrset-member. No TTL parameter: the member is identified by its composite key, whose digest is over the canonical rdata alone.
One fence-guarded transaction that asserts the instance fence FIRST, runs the answerability guard, then deletes at most one row keyed by (zoneid, owner, recordtype, rdatadigest) AND guarded by (:not transient) — so a durable delete can only ever remove a durable record and never a transient turn-up member. When a row is removed the FULL invariant set is re-asserted over the resulting served set — a delete that would strand the zone without its apex SOA or apex NS signals the specific zone-invariant-violation and rolls the delete back — then the serial advances once and the zone-committed-hook fires OUTSIDE the transaction. Deleting an absent durable member affects zero rows and is an idempotent no-op: NO invariant re-check, NO serial bump, NO hook. A co-resident sibling member at the same name (distinct rdata) is untouched. Runs inside the caller's pooled operator-state connection. Returns (values RESOLVED-SERIAL CHANGED-P UNSERVED-REASON), where UNSERVED-REASON is NIL when the edit also reached the serving side and otherwise names why it did not.
delete-zone
(delete-zone origin my-generation)
Delete the zone at ORIGIN in one atomic, fence-guarded transaction — removing the apex/SOA row IS removing the zone. ORIGIN is canonicalised defensively (idempotent on an already-absolute, dotted FQDN). MY-GENERATION is the live store write-epoch this instance holds.
The write discipline mirrors import-zone-master-text: open one logical transaction, assert the instance fence FIRST (a superseded instance signals fenced-out and deletes nothing — the split-brain stop), then delete the zone's records and its apex row as explicit s-sql (records before the zone row so the referential foreign key never blocks the delete). The whole delete commits or rolls back as one unit. Deleting an absent zone affects zero rows and returns cleanly (idempotent).
Runs inside the caller's pooled operator-state connection (the axis wraps this in with-operator-state-connection on a worker thread). Returns the number of zone rows removed (1 for a present zone, 0 for an absent one).
dns-record-table-ddl
(dns-record-table-ddl)
Return the CREATE TABLE statement for the dnsrecord table, derived from the dao-class slots. Its composite primary key covers (zoneid, owner, recordtype, rdatadigest) — the bounded rrset-dedup constraint — and never the unbounded rdata text.
dns-zone-table-ddl
(dns-zone-table-ddl)
Return the CREATE TABLE statement for the dnszone table, derived from the dao-class slots so the schema and the DAO never drift. The migration runner and the confused-deputy audit both consume this string.
exactly-one-apex-soa
(exactly-one-apex-soa records origin)
Signal missing-apex-soa unless RECORDS contains exactly one SOA in total AND that SOA is owned by the canonical apex ORIGIN. A zone has precisely one SOA, at its apex; a missing apex SOA, more than one SOA anywhere, or a lone SOA at a non-apex name are all rejected. Counting total SOAs (not just apex SOAs) is load-bearing: a stray non-apex SOA is the LAST SOA the parser records, so without this the import would read the zone's serial/SOA metadata from the wrong record.
export-durable-zone-master-text
(export-durable-zone-master-text origin)
Render the DURABLE records of the zone at canonical ORIGIN as canonical master text — sorted presentation lines, one per line, one trailing newline — for the operator's export tooling. Filters the stored rows to the durable set (transient = nil), so a live transient member (a published challenge) is NEVER baked into the exported master; a durable export therefore round-trips a published→retracted cycle without pollution.
This is a SEPARATE reader from the serving surface: the serving read (zone-record-set / lookup-records) must keep returning transient rows so the CA can resolve a live challenge through the normal answer path. Only the export filters them out. Each durable row crosses to runciter through row->resource-record and renders via record-canonical-line, the one source of truth for the line format, so the export is byte-identical to the durable slice of the serving/AXFR view.
Runs inside the caller's pooled operator-state connection. A re-import of the exported master assumes a single writer: were a durable record here to later collide, on re-import, with a surviving transient member's composite key, that re-insert would raise a primary-key violation — detect-and-republish is the challenge manager's job, not this export's. Returns the master text string.
import-zone-master-text
(import-zone-master-text master-text origin my-generation &key owner-did)
Import RFC 1035 MASTER-TEXT as the atomic full-zone replacement of the zone at ORIGIN: the adoption door for a zone taken in whole, from a BIND server or a file. A live zone is edited through the typed record verbs on the :names axis instead; master text is an interchange format at the exterior-nameserver boundary, not the internal representation and not the edit format. ORIGIN must be an absolute, dotted FQDN. MY-GENERATION is the live store write-epoch this instance holds. OWNER-DID, when supplied, is the zone's write-authority DID (the axis captures it from the session principal); on a re-import it defaults to the stored owner.
A thin text FRONT over commit-zone-records: parse the master text to runciter-typed records (refusing $INCLUDE on operator-submitted text rather than reading host paths), then commit through the shared provenance-parameterized door with an :operator provenance built from OWNER-DID, landing the zone at the :primary cutover state. All the write discipline — the structural invariants, the fence-first transaction, RFC 1982 serial monotonicity, the full-zone replace, and the ndbentry stamping — lives in commit-zone-records.
Runs inside the caller's pooled operator-state connection (the axis wraps this in with-operator-state-connection on a worker thread). Returns the resolved serial.
list-durable-zone-records
(list-durable-zone-records origin)
Return the DURABLE records of the zone at canonical ORIGIN as a list of plists, each (:owner :ttl :class :type :rdata :digest) — the structured owner-editing view a table renders and targets edits against without re-parsing rendered master text. :rdata is the stored canonical presentation text and :digest its bounded rdata digest, the stable per-member identity the delete/replace verbs address. Read straight from the stored columns (no runciter round-trip), so a corrupt stored rdata never fails the listing. Transient turn-up rows are excluded — they are not the owner's editing surface, exactly as the durable master export excludes them. Runs inside the caller's pooled operator-state connection.
make-provenance
(make-provenance &key (kind :operator) principal upstream-master transfer-event)
Construct a provenance descriptor for the commit gate. (make-provenance :kind :operator :principal <did>) (make-provenance :kind :secondary :principal <controlling-operator-did> :upstream-master <public-ref> :transfer-event (list :type :axfr :serial s :at now)) ownerdid derives from PRINCIPAL for both kinds (control is not authorship); the secondary facets ride on UPSTREAM-MASTER / TRANSFER-EVENT.
names-within-zone
(names-within-zone records origin)
Signal name-outside-zone if any record owner, canonicalised under ORIGIN with the two-arg (owner origin) form, is not at or under the canonical zone ORIGIN. A name is in-zone when it equals the origin or ends in '.<origin>'.
no-duplicate-rrset-members
(no-duplicate-rrset-members records origin)
Signal duplicate-rrset-member if two records share the same canonical (owner, type, rdata-digest) — the application-level mirror of the (zoneid, owner, recordtype, rdatadigest) composite key. Keyed on the bounded rdata digest, never the unbounded rdata text.
owned-zone-origins
(owned-zone-origins)
Return (values ORIGINS STATUS) — the canonical origins of the zones this instance holds ON SOMEONE'S BEHALF and answers for, sorted, and a keyword saying which empty answer an empty ORIGINS is.
A zone qualifies when its dnszone row names a real owner principal AND its ndbentry anchor is at cutover-state primary or answer-as-secondary. Both halves are required and they live in different tables, so the two are joined rather than one being picked: ownerdid is entry control and sits on dnszone; cutover-state is an authority facet and sits on ndbentry, and the two can disagree.
⛔ A :held zone is refused however valid its owner. Admission turns on operator INTENT, and the cutover flip is the deliberate act that means "this zone is ours to answer now". During an adoption the live site still belongs to the incumbent, so serving that name would answer someone else's traffic under a certificate valid for their domain — a worse failure than a TLS error.
⛔ The gate is cutover-state itself, never may-answer-p, which folds RFC 2308 freshness in. This answer is taken at boot and on rescan, never per handshake, so it cannot track an expiry timer; gating on a time-varying predicate would make the result silently drift from what it reports.
An owner principal is a non-null value that is neither blank nor the string "false". ⚠ The blank and "false" forms are historical. A zone committed with no principal at all once stored the text "false", because the commit door marshalled an absent principal into a text column instead of writing SQL NULL; that defect has since been corrected and the column is nullable. The check still refuses those forms because a row written before the fix, or by a node running an older binary, can carry them. Refusing them is what makes this verb answer "held on someone's behalf" rather than "present".
STATUS is :QUERIED when operator state answered — an empty ORIGINS then means no zone qualifies — and :OPERATOR-STATE-UNREACHABLE when it could not be read at all. The two must stay tellable apart: collapsed into one empty list they look identical, and a node that owns nothing is indistinguishable from a node whose every name was silently refused. ⚠ The verb never signals: it can legitimately be called before the pool is armed, and a condition escaping here would take bring-up down with it.
provenance-kind
(provenance-kind instance)
Undocumented: this exported symbol needs a docstring.
provenance-p
(provenance-p object)
Undocumented: this exported symbol needs a docstring.
provenance-principal
(provenance-principal instance)
Undocumented: this exported symbol needs a docstring.
provenance-transfer-event
(provenance-transfer-event instance)
Undocumented: this exported symbol needs a docstring.
provenance-upstream-master
(provenance-upstream-master instance)
Undocumented: this exported symbol needs a docstring.
publish-transient-rrset-member
(publish-transient-rrset-member origin owner type ttl rdata my-generation)
Publish ONE transient rrset member (OWNER TYPE TTL RDATA) into the live :primary zone at ORIGIN, held under the store write-epoch MY-GENERATION. TYPE is a runciter RR-type keyword (e.g. :txt); RDATA is its presentation text, crossed to runciter:parse-rdata (the sole RR-syntax authority) and rendered canonical so the stored columns are byte-identical to the import path.
TYPE :soa is refused up front (transient-soa-refused): a zone holds a single SOA at its apex, so a transient SOA is never a valid member. NS, a legitimate multi-member rrset, is not refused.
Mirrors commit-zone-records' transaction shape at single-row granularity, and NEVER the destructive full-zone path: one logical transaction that asserts the instance fence FIRST (the split-brain stop), runs the answerability guard, then resolves the member against its composite key (zoneid, owner, recordtype, rdatadigest):
- a DURABLE row at that key → signal transient-durable-collision, mutate nothing (publishing never shadows or later deletes a durable record);
- the identical TRANSIENT member already present → idempotent no-op, NO serial bump;
- absent → the CNAME-exclusivity guard runs, then a single row inserts with transient=t and the zone serial advances once via the RFC 1982 auto-bump.
On a real change the zone-committed-hook fires OUTSIDE the transaction with (canonical-origin resolved-serial) so the outbound feed reaches secondaries; a no-op neither bumps nor fires. Co-resident members of the same rrset (distinct rdata → distinct digest) are independent rows, so add/remove never overwrites a sibling member. Runs inside the caller's pooled operator-state connection. Returns (values RESOLVED-SERIAL CHANGED-P).
rdata-columns->canonical-line
(rdata-columns->canonical-line owner ttl class type rdata)
Reconstruct a canonical 'owner ttl CLASS TYPE rdata' line from the stored columns. Byte-identical to runciter's record-canonical-line by construction (same format string, same field order), so a PG round-trip preserves the parse.
rdata-digest
(rdata-digest rdata-text)
Return a deterministic lowercase hex SHA-256 digest of RDATA-TEXT — the bounded fixed-width value the (zoneid, owner, recordtype, rdatadigest) composite key indexes, so rrset dedup never indexes the unbounded rdata presentation text. Mirrors the in-tree digest idiom (src/store/block.lisp, src/capability/*).
record->rdata-columns
(record->rdata-columns record)
Decompose a parsed RESOURCE-RECORD into the dnsrecord columns valis stores: owner, ttl, the upcased CLASS and TYPE keywords, and the rdata presentation text. The rdata is rendered by runciter:render-rdata — valis renders no RR syntax of its own. Returns (values owner ttl class type rdata).
replace-durable-rrset-member
(replace-durable-rrset-member origin owner type old-rdata new-ttl new-rdata my-generation &key owner-did)
Replace ONE DURABLE rrset member at (OWNER TYPE OLD-RDATA) with (OWNER TYPE NEW-TTL NEW-RDATA) in the :primary zone at ORIGIN, as one ATOMIC delete+add: the old durable member is removed and the new one established in a single fence-guarded transaction, the FULL invariant set is re-asserted once over the result, and the zone serial bumps once. TYPE is unchanged across the replace (an rrset member keeps its type; a type change is a delete plus a distinct add). OLD-RDATA and NEW-RDATA are presentation text typed by runciter:parse-rdata.
The old member MUST exist as a DURABLE record — a replace targets an existing member, so its absence signals durable-member-not-found and rolls back rather than degrading to a bare add. The new member is CNAME-exclusivity-checked when genuinely new; a transient row at the new key is promoted to durable; and when the new rdata coincides with a DIFFERENT durable member still present at the same (owner, type) — never the one just deleted — the replace round-trips to a TTL refresh of that member. When the old and new rdata are identical the old row is removed first, so the new member is re-inserted rather than refreshed. On success the zone-committed-hook fires OUTSIDE the transaction. Runs inside the caller's pooled operator-state connection. Returns (values RESOLVED-SERIAL CHANGED-P UNSERVED-REASON) — CHANGED-P is always true on a committed replace, and UNSERVED-REASON is NIL when the edit also reached the serving side and otherwise names why it did not.
resolve-serial
(resolve-serial incoming stored)
Resolve the serial to persist for a zone import under serial monotonicity, using RFC 1982 arithmetic (version-newer-p), never CL:< . A brand-new zone (STORED nil) takes the INCOMING serial, or 0 when the incoming serial is also absent (never NIL); an advancing serial is honored; an equal or absent serial auto-bumps to stored+1 (mod 232) so a re-import of unchanged text still advances the wire-visible serial; a strictly-older serial is refused (signals zone-serial-regression) — a zone serial is never lowered.
INCOMING is checked for nil before any RFC 1982 comparison so an absent serial never reaches version-newer-p / = with a nil operand.
retract-transient-rrset-member
(retract-transient-rrset-member origin owner type rdata my-generation)
Retract ONE transient rrset member (OWNER TYPE RDATA) from the zone at ORIGIN, held under the store write-epoch MY-GENERATION. No TTL parameter: the member is identified by its composite key, whose digest is over the canonical rdata alone.
Mirrors delete-zone's minimal fence-guarded delete transaction, and NEVER the full-zone path: one logical transaction that asserts the instance fence FIRST, then deletes at most one row keyed by (zoneid, owner, recordtype, rdatadigest) AND guarded by (:= 'transient t) — so a retract can only ever remove a TRANSIENT row and never a durable record. When the delete affects a row the zone serial advances once via the RFC 1982 auto-bump and the zone-committed-hook fires OUTSIDE the transaction; retracting an absent member affects zero rows and is an idempotent no-op — NO serial bump, NO hook. A co-resident sibling member at the same name (distinct rdata) is untouched, so it stays served. Runs inside the caller's pooled operator-state connection. Returns (values RESOLVED-SERIAL CHANGED-P).
validate-zone-records
(validate-zone-records records canonical-origin)
Run every structural invariant valis owns over the flat RECORDS sequence at CANONICAL-ORIGIN, signalling the specific zone-invariant-violation on the first failure and returning T when all hold. runciter has already guaranteed each RR is syntactically valid and canonical; this is the relational layer valis adds.
The records-direct entry point both the text door and the secondary door share: the secondary path receives runciter-typed records already (no zone wrapper to unwrap), so the invariants run over the sequence directly.
validate-zone-structure
(validate-zone-structure zone canonical-origin)
Run every structural invariant valis owns over the parsed ZONE at CANONICAL-ORIGIN, signalling the specific zone-invariant-violation on the first failure and returning T when all hold. runciter has already guaranteed each RR is syntactically valid and canonical; this is the relational layer valis adds.
Thin wrapper over validate-zone-records: unwraps the parsed zone to its records sequence so the text and secondary paths validate through one shared body.
Macros
with-transient-rrset-member
(with-transient-rrset-member (origin owner type ttl rdata my-generation) &body body)
Publish the transient rrset member (ORIGIN OWNER TYPE TTL RDATA) under the store write-epoch MY-GENERATION, evaluate BODY, and retract the member on EVERY exit path — normal return, non-local exit, or error — via unwind-protect. Returns BODY's value. Placement-agnostic: MY-GENERATION is an ordinary argument, never read from an assumed caller context.
The publish and the retract are each their OWN fence-asserted transaction; the macro does NOT wrap them in a single transaction — it only guarantees the retract runs. It assumes the caller OWNS the member for the macro's dynamic extent (single- caller, in-process, or test use). The production ACME caller owns its own unwind-protect and need not use this macro for a shared _acme-challenge name that other issuances may co-populate.
Variables
*zone-committed-hook*
An inverted, post-commit hook fired by commit-zone-records after the full-zone replace commits durably: a function of (canonical-origin resolved-serial), or NIL for no-op. The outbound-feed module (zone-feed) installs fire-zone-change onto it at load so BOTH the operator text-import and the secondary-ingest paths emit the in-process zone-change signal off this one shared commit point. Inverted (zones names no feed package; the feed module sets this var) so there is no circular package dependency. Fired OUTSIDE the transaction, on success only, so a listener observes a durable change and a listener error cannot roll the commit back.