# ol.protocol53 One Clojure API for managing DNS records across providers ## ol.protocol53.cloudflare # ol.protocol53.cloudflare Cloudflare DNS provider for protocol53. ## ->Provider ```clojure (->Provider api-token zone-token http-client) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/cloudflare/src/main/ol/protocol53/cloudflare.clj#L643-L678) --- ## Provider [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/cloudflare/src/main/ol/protocol53/cloudflare.clj#L643-L678) --- ## map->Provider ```clojure (map->Provider m) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/cloudflare/src/main/ol/protocol53/cloudflare.clj#L643-L678) --- ## provider ```clojure (provider config) ``` Returns a Cloudflare provider from `config`. Options: | | | | --- | --- | | key | description | | `:api-token` | Scoped token used for DNS record requests. | | `:zone-token` | Optional scoped token used for zone reads. | | `:http-client` | Optional `java.net.http.HttpClient` for advanced HTTP policy. | [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/cloudflare/src/main/ol/protocol53/cloudflare.clj#L680-L692) ## ol.protocol53.deadline # ol.protocol53.deadline Time budgets for protocol53 operations. A deadline is a fixed point in the future that answers one question: has an operation run out of time? Callers usually pass a positive `java.time.Duration` under `:timeout`; the protocol53 facade converts it to the `:deadline` in normalized provider options. Callers may instead create a deadline when several operations must share one budget. Deadlines are monotonic. They ride on `System/nanoTime`, so clock changes — NTP corrections, daylight saving, someone setting the system clock — cannot move them. That makes them reliable for timeouts and meaningless for anything else: a deadline is just a number that only makes sense inside the process that created it. Do not serialize it, store it, share it across processes, or compare it against wall-clock time. Build one with [`after`](#after). Check it with [`remaining`](#remaining), [`expired?`](#expired?), and [`within?`](#within?). ## after ```clojure (after duration) ``` Returns a deadline that falls `duration` from now. `duration` is a positive `java.time.Duration`; it must be shorter than `Long/MAX_VALUE` nanoseconds, roughly 292 years. ```clojure (deadline/after (java.time.Duration/ofSeconds 30)) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/deadline.clj#L42-L53) --- ## remaining ```clojure (remaining operation-deadline) ``` Returns how much time is left before `operation-deadline`. The value is a `java.time.Duration` that never goes negative; once the deadline has passed it is `Duration/ZERO`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/deadline.clj#L55-L67) --- ## expired? ```clojure (expired? operation-deadline) ``` Returns `true` once `operation-deadline` has no time left. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/deadline.clj#L69-L72) --- ## within? ```clojure (within? operation-deadline duration) ``` Returns `true` when `duration` still fits before `operation-deadline`. Use it to decide whether to start work that needs at least `duration` of budget. `duration` is a positive `java.time.Duration`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/deadline.clj#L74-L81) ## ol.protocol53.desec # ol.protocol53.desec deSEC DNS provider for protocol53. ## ->Provider ```clojure (->Provider token http-client) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/desec/src/main/ol/protocol53/desec.clj#L545-L580) --- ## Provider [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/desec/src/main/ol/protocol53/desec.clj#L545-L580) --- ## map->Provider ```clojure (map->Provider m) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/desec/src/main/ol/protocol53/desec.clj#L545-L580) --- ## provider ```clojure (provider config) ``` Returns a deSEC provider from `config`. Options: | key | description | -------------- | ----------- | `:token` | deSEC API token used for domain and RRset requests. | `:http-client` | Optional `java.net.http.HttpClient` for advanced HTTP policy. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/desec/src/main/ol/protocol53/desec.clj#L582-L593) ## ol.protocol53.http # ol.protocol53.http Synchronous HTTP transport for bundled protocol53 providers. This namespace is an internal provider utility, not a general-purpose HTTP client. Loading it requires Java 11 or newer. Advanced client policy remains available through a caller-supplied `java.net.http.HttpClient`. ## request ```clojure (request request) ``` Sends one synchronous HTTP request and returns its response. Options: | key | description | ----------------|------------ | `:uri` | Required URI string or component map. | `:method` | HTTP method keyword or string (default `:get`). | `:headers` | Map of header names to string or sequential values. | `:query-params` | Map of UTF-8 form-encoded query parameters. | `:form-params` | Map encoded as the request body. | `:body` | String, `java.io.File`, or `java.io.InputStream`. | `:as` | `:string` (default) or `:stream`. | `:timeout` | Request timeout in milliseconds. | `:throw` | Throw for exceptional statuses unless `false`. | `:version` | `:http1.1` or `:http2` (default `:http2`). | `:client` | `java.net.http.HttpClient` or request function. Returns `:status`, vector-valued `:headers`, `:body`, and the negotiated `:version`. The body is a string by default. With `:as :stream`, it is a `java.io.InputStream` available after the response headers; the caller must exhaust or close it. Streaming callers should normally use `:throw false` to retain ownership of the stream for every status. Exceptional statuses throw `ExceptionInfo` with the complete response as `ex-data`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/http.clj#L200-L231) ## ol.protocol53.malli # ol.protocol53.malli Optional Malli schemas for protocol53 inputs and outcomes. This namespace requires consumers to supply Malli. Core protocol53 namespaces do not load it. ## record Schema for a complete portable record. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L25-L32) --- ## record-selector Schema for a delete selector with wildcard fields. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L34-L41) --- ## records Schema for a vector of complete records. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L43-L45) --- ## record-selectors Schema for a vector of delete selectors. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L47-L49) --- ## zone Schema for one zone. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L51-L53) --- ## zones Schema for a vector of zones. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L55-L57) --- ## opts Describes open caller options with exactly one time budget. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L59-L66) --- ## provider-opts Describes open normalized provider options with only `:deadline`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L68-L73) --- ## record-result-data Schema for the inner result of a record operation. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L81-L83) --- ## zone-result-data Schema for the inner result of zone listing. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L85-L87) --- ## error-data Schema for inner error data. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L89-L105) --- ## record-success Schema for a successful record-operation outcome. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L113-L116) --- ## zone-success Schema for a successful zone-listing outcome. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L118-L121) --- ## error-outcome Schema for an error outcome. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L123-L126) --- ## record-outcome Schema for exactly one record result or error. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L128-L133) --- ## zone-outcome Schema for exactly one zone result or error. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L135-L140) --- ## get-records-fn Function schema for `ol.protocol53/get-records!`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L142-L145) --- ## append-records-fn Function schema for `ol.protocol53/append-records!`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L147-L151) --- ## set-records-fn Function schema for `ol.protocol53/set-records!`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L153-L155) --- ## delete-records-fn Function schema for `ol.protocol53/delete-records!`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L157-L161) --- ## list-zones-fn Function schema for `ol.protocol53/list-zones!`. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/malli.clj#L163-L166) ## ol.protocol53.porkbun # ol.protocol53.porkbun Porkbun DNS provider for protocol53. ## ->Provider ```clojure (->Provider api-key secret-key http-client) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/porkbun/src/main/ol/protocol53/porkbun.clj#L707-L742) --- ## Provider [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/porkbun/src/main/ol/protocol53/porkbun.clj#L707-L742) --- ## map->Provider ```clojure (map->Provider m) ``` [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/porkbun/src/main/ol/protocol53/porkbun.clj#L707-L742) --- ## provider ```clojure (provider config) ``` Returns a Porkbun provider from `config`. Options: | | | | --- | --- | | key | description | | `:api-key` | Porkbun API key. | | `:secret-key` | Porkbun secret API key. | | `:http-client` | Optional `java.net.http.HttpClient` for advanced HTTP policy. | [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/providers/porkbun/src/main/ol/protocol53/porkbun.clj#L756-L768) ## ol.protocol53.protocols # ol.protocol53.protocols Provider capability extension points. ## RecordGetter Retrieves all records in one zone. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/protocols.clj#L4-L6) ### -get-records! ```clojure (-get-records! provider zone opts) ``` --- ## RecordAppender Appends records without modifying existing records. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/protocols.clj#L8-L10) ### -append-records! ```clojure (-append-records! provider zone records opts) ``` --- ## RecordSetter Replaces the selected complete RRsets. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/protocols.clj#L12-L14) ### -set-records! ```clojure (-set-records! provider zone records opts) ``` --- ## RecordDeleter Deletes matching records and ignores misses. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/protocols.clj#L16-L18) ### -delete-records! ```clojure (-delete-records! provider zone records opts) ``` --- ## ZoneLister Lists zones supported by record operations. _protocol_ [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53/protocols.clj#L20-L22) ### -list-zones! ```clojure (-list-zones! provider opts) ``` ## ol.protocol53.specs # ol.protocol53.specs Specifications for protocol53 inputs and outcomes. ## ol.protocol53.testkit.dotenv # ol.protocol53.testkit.dotenv Minimal `.env` file loading for development lifecycle suites. ## load-env-file ```clojure (load-env-file filename) ``` Loads assignments from `filename` into a string map. Returns an empty map when `filename` does not name an existing file. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/testkit/src/main/ol/protocol53/testkit/dotenv.clj#L36-L43) ## ol.protocol53.testkit # ol.protocol53.testkit Provider-agnostic lifecycle checks for protocol53 implementations. This suite creates, replaces, and deletes DNS records. Run it only against a dedicated disposable test zone. Generated owners use the reserved `p53test-` prefix so interrupted runs can be cleaned safely. ## cleanup! ```clojure (cleanup! config) ``` Removes reserved lifecycle records and verifies their absence. Use this after an interrupted run. The function changes DNS and rejects the call unless `:allow-live-changes?` is exactly `true`. Options: | | | | --- | --- | | key | description | | `:provider` | Provider implementing all four record capabilities (required). | | `:zone` | Dedicated absolute zone name ending in `.` (required). | | `:allow-live-changes?` | Destructive-operation acknowledgement; must be `true` (required). | | `:timeout` | Positive `java.time.Duration` for each call (default 30 seconds). | | `:skip-record-types` | Set of nonessential uppercase or lowercase types (default `#{}`). | | `:skip-empty-txt?` | Omit the exact-empty TXT fixture when unsupported (default `false`). | [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/testkit/src/main/ol/protocol53/testkit.clj#L256-L273) --- ## run! ```clojure (run! config) ``` Runs the provider lifecycle suite against a dedicated DNS zone. The suite validates configuration, cleans stale `p53test-` records, and then runs list, get, append, set, delete, wildcard-delete, and final cleanup checks sequentially. It emits `clojure.test` assertions in the active test. Options: | | | | --- | --- | | key | description | | `:provider` | Provider implementing all four record capabilities (required). | | `:zone` | Dedicated absolute zone name ending in `.` (required). | | `:allow-live-changes?` | Destructive-operation acknowledgement; must be `true` (required). | | `:timeout` | Positive `java.time.Duration` for each call (default 30 seconds). | | `:skip-record-types` | Set of nonessential uppercase or lowercase types (default `#{}`). | | `:skip-empty-txt?` | Omit the exact-empty TXT fixture when unsupported (default `false`). | | `:ignore-ttl?` | Ignore TTL differences in lifecycle checks (default `false`). | | `:expect-empty-zone?` | Reject remaining active non-`NS` records (default `false`). | [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/testkit/src/main/ol/protocol53/testkit.clj#L444-L477) ## ol.protocol53 # ol.protocol53 The DNS operations you call independent of any one provider. protocol53 gives you a small, fixed set of operations — [`get-records!`](#get-records!), [`append-records!`](#append-records!), [`set-records!`](#set-records!), [`delete-records!`](#delete-records!), and [`list-zones!`](#list-zones!) — that behave the same behind every supported DNS provider. Write against these functions once and switch providers, such as Cloudflare, without changing your call sites. A provider is any value that implements the capabilities in [`ol.protocol53.protocols`](api/ol-protocol53-protocols.adoc). You build one from its own namespace, for example `ol.protocol53.cloudflare/provider`, and pass it as the first argument to every operation. Each operation takes trailing `opts` with exactly one time budget. A positive `java.time.Duration` under `:timeout` starts a fresh budget for the call. A process-local `:deadline`, created with [`ol.protocol53.deadline/after`](api/ol-protocol53-deadline.adoc#after), lets several calls share one budget. The facade normalizes both forms to a deadline before provider dispatch. Operations do not throw on expected failures. Each returns a map with exactly one of these keys: * `:ol.protocol53/result` on success, holding the records or zones produced. * `:ol.protocol53/error` on failure, describing what went wrong and whether a retry might succeed. So a typical call checks for `:ol.protocol53/error` before using the result. ## get-records! ```clojure (get-records! provider zone opts) ``` Reads every record in `zone` and returns them in the result. Read-only; it never changes the zone. On success the result holds a vector of records under `:records`. Supply exactly one time-budget option. Options: | key | description | ----------- | ----------- | `:timeout` | Positive `java.time.Duration` starting a fresh budget. | `:deadline` | Process-local deadline for sharing an existing budget. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53.clj#L85-L102) --- ## append-records! ```clojure (append-records! provider zone records opts) ``` Adds `records` to `zone`, leaving every existing record in place. Use this to create records without disturbing what is already there. On success the result lists the records as the provider stored them. Supply exactly one time-budget option. Options: | key | description | ----------- | ----------- | `:timeout` | Positive `java.time.Duration` starting a fresh budget. | `:deadline` | Process-local deadline for sharing an existing budget. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53.clj#L104-L131) --- ## set-records! ```clojure (set-records! provider zone records opts) ``` Replaces whole RRsets in `zone` with `records`. For each `name`/`type` pair present in `records`, this removes every existing record of that name and type and installs the ones you gave. Names and types you do not mention are left untouched. Use it to declare the exact desired state of the RRsets you care about. Supply exactly one time-budget option. Options: | key | description | ----------- | ----------- | `:timeout` | Positive `java.time.Duration` starting a fresh budget. | `:deadline` | Process-local deadline for sharing an existing budget. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53.clj#L133-L161) --- ## delete-records! ```clojure (delete-records! provider zone records opts) ``` Removes records in `zone` that match `records` and ignores the rest. Each entry in `records` is a selector: it must name a record and may narrow the match by `type`, `ttl`, or `data`. A selector that matches nothing is not an error. On success the result lists the records that were actually removed. Supply exactly one time-budget option. Options: | key | description | ----------- | ----------- | `:timeout` | Positive `java.time.Duration` starting a fresh budget. | `:deadline` | Process-local deadline for sharing an existing budget. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53.clj#L163-L192) --- ## list-zones! ```clojure (list-zones! provider opts) ``` Lists the zones `provider` can operate on. On success the result holds a vector of zone maps under `:zones`. Use it to discover which zones the other operations may target. Supply exactly one time-budget option. Options: | key | description | ----------- | ----------- | `:timeout` | Positive `java.time.Duration` starting a fresh budget. | `:deadline` | Process-local deadline for sharing an existing budget. [source,window=_blank](https://github.com/outskirtslabs/protocol53/blob/main/api/src/main/ol/protocol53.clj#L194-L210) ## ol.protocol53 # ol.protocol53 > One Clojure API for managing DNS records across providers.    `ol.protocol53` gives Clojure applications one provider-independent API for querying and changing DNS records. Applications can switch DNS services without changing their record-management code. The shared API lives in `api/`. Provider integrations live in `providers/` and translate the common operations into each service’s API. Protocol53 supports: * retrieving records from a zone * adding records * creating or replacing whole RRsets * removing matching records * listing available zones Provider capabilities and limitations vary, but applications use the same core abstractions across every supported integration. Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**. ## Available Providers * [Cloudflare](https://docs.outskirtslabs.com/ol.protocol53/next/providers/cloudflare) * [deSEC](https://docs.outskirtslabs.com/ol.protocol53/next/providers/desec) * [Porkbun](https://docs.outskirtslabs.com/ol.protocol53/next/providers/porkbun) ## Installation Until the first release, use the API’s Git coordinate: ```clojure {:deps {com.outskirtslabs/protocol53 {:git/url "https://github.com/outskirtslabs/protocol53.git" :git/sha "4a0ad834256d7e1e1a6a95936ea5e7ca14f13988" :deps/root "api"}}} ``` ## Quick Start Create a provider, then pass it to the common operations with a time budget: ```clojure (require '[ol.protocol53 :as protocol53] '[ol.protocol53.cloudflare :as cloudflare]) (import '[java.time Duration]) (def dns (cloudflare/provider {:api-token (System/getenv "CLOUDFLARE_API_TOKEN")})) (protocol53/append-records! dns "example.com" [{:name "_acme-challenge" :type "TXT" :ttl 60 :data "challenge-token"}] {:timeout (Duration/ofSeconds 10)}) ``` Operations return either `:ol.protocol53/result` or `:ol.protocol53/error`; expected provider failures do not throw. ## Documentation * [Docs](https://docs.outskirtslabs.com/ol.protocol53/next/) * [API Reference](https://docs.outskirtslabs.com/ol.protocol53/next/api) * [Support via GitHub Issues](https://github.com/outskirtslabs/protocol53/issues) ## Scope `ol.protocol53` targets common DNS record operations rather than every provider-specific feature. DNS record types and provider APIs vary too much for a universal abstraction, so Protocol53 focuses on the operations applications use across providers. ## Related Projects [`ol.clave`](https://docs.outskirtslabs.com/ol.clave/next/) is Outskirts Labs' ACME client for automated HTTPS certificate management. ACME DNS-01 validation needs temporary TXT records, which Protocol53 can manage across its supported DNS providers. A direct Clave adapter remains future work. * [Clave documentation](https://docs.outskirtslabs.com/ol.clave/next/) * [Clave source code](https://github.com/outskirtslabs/clave) ## License Copyright (C) 2026 Casey Link mailto:casey@outskirtslabs.com[casey@outskirtslabs.com] Distributed under the [MIT](https://spdx.org/licenses/MIT.html) license. ## Cloudflare provider # Cloudflare provider The [Cloudflare](https://cloudflare.com) provider implements all [Protocol53](https://github.com/outskirtslabs/protocol53) operations with Cloudflare’s v4 API. ## Installation Until the first release, use the provider’s Git coordinate: ```clojure {:deps {com.outskirtslabs/protocol53-cloudflare {:git/url "https://github.com/outskirtslabs/protocol53.git" :git/sha "4a0ad834256d7e1e1a6a95936ea5e7ca14f13988" :deps/root "providers/cloudflare"}}} ``` The provider module also brings in the shared Protocol53 API. ## Authentication Use scoped API tokens, not legacy global API keys. For one token, grant `Zone:Read` and `Zone.DNS:Write` across the zones you manage: ```clojure (require '[ol.protocol53.cloudflare :as cloudflare]) (def dns (cloudflare/provider {:api-token "token"})) ``` For separate read and write credentials, grant `Zone:Read` to `:zone-token` and `Zone.DNS:Write` to `:api-token`: ```clojure (def dns (cloudflare/provider {:api-token "dns-write-token" :zone-token "zone-read-token"})) ``` Pass a caller-built `java.net.http.HttpClient` as `:http-client` when advanced HTTP policy requires Java interop. ## Integration Tests