# Outskirts Labs Docs
## Contributing Guide
# Contributing Guide
I welcome third party contributions.
The most straightforward way to do so, is to fork the project and submit a PR.
If you want to contribute even something trivial please do not hesitate, but follow the guidelines below.
If the change you’re proposing is substantial, _please open an issue in the issue tracker in order to discuss it first_.
By submitting a pull request, you agree to license the submitted code under
[the EUPL-1.2 License](https://eupl.eu/1.2/en/) (or whatever the project license actually is).
If you do not own the copyright of the code that is being submitted, please clarify the name of the copyright holder and the license under which the copyrighted material can be used so that we can review if it could be incorporated as part of the project.
If you have questions about contributing contact [Casey](https://casey.link/about) directly :)
## Environment Setup
Nearly all OL projects use nix flake devshells. Getting started is easy, and it works on every platform.
1. Install Nix. Follow the one pager at https://zero-to-nix.com/start/install/
2. Clone the project git repository
3. Run `nix develop` (if you use a different shell: `nix develop --command zsh`)
Tada, now you have a working dev environment.
## Common Development Commands
I have more-or-less standardized on a common set of babashka tasks and a common `bb.edn` across all projects.
```shell
bb fmt # run the formatter
bb lint # run the linter
bb test # run the test suite
```
All three of these must come back clean/green before committing and sharing a PR.
## Continuous Integration (CI)
Most projects use continuous integration to check the commits.
The CI jobs are triggered by pushing to the Git repository and are listed on Github Actions.
If a PR of yours fails a check, be proactive to figure out what failed.
## AI/LLM Policy
If you use LLMs for your contribution (issue, comment, PR), it’s now your job to check and clean up its output, just like with any other tool. You have to spend the time doing this.
Using AI to write code is fine.
What’s not fine is submitting agent-generated slop without understanding it.
You must be able to explain what your changes do and how they interact with the rest of the system. If you can’t, your PR will be closed.
In particular:
* If I can tell that a PR is mostly LLM-generated via the usual indicators that violate the conventional style of the codebase, it’ll take significantly more time/effort than usual to review and finish. I’m not interested in such PRs.
* When using an LLM to write an issue, the text usually has a lot of fluff. No one wants to read it. Clean up the text and keep only the relevant stuff. I care more about accuracy and completeness (but not verbosity!).
* When using an LLM to comment on an issue, you still have to verify that the comment makes sense, contributes something useful, and doesn’t repeat itself unnecessarily. (Yes, irony. If I repeat myself enough in this policy, maybe your LLM will understand)
## datahike-sqlite.core
# datahike-sqlite.core
## datahike-sqlite.konserve
# datahike-sqlite.konserve
## default-table
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L18-L18)
---
## *batch-insert-strategy*
DO NOT USE! For testing/dev only!
Strategy for batch inserts: :sequential, :multi-row
:sequential - Execute individual INSERT statements for each key-value pair
:multi-row - Use multi-row INSERT VALUES syntax
Preliminary testing shows that :sequential is always faster than :multi-row
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L20-L26)
---
## with-write-tx
```clojure
(with-write-tx db f)
```
Wrapper around the with-write-tx macro, for use in situations where we cannot use macros directly.
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L28-L32)
---
## init-db
```clojure
(init-db db-spec)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L34-L40)
---
## create-statement
```clojure
(create-statement table)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L42-L43)
---
## upsert-statement
```clojure
(upsert-statement table id header meta value)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L45-L49)
---
## select-exists-statement
```clojure
(select-exists-statement table id)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L51-L52)
---
## delete-row-statement
```clojure
(delete-row-statement table store-key)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L54-L55)
---
## select-row-statement
```clojure
(select-row-statement table id)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L57-L58)
---
## select-table-exists-statement
```clojure
(select-table-exists-statement table)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L60-L61)
---
## select-all-ids-statement
```clojure
(select-all-ids-statement table)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L63-L64)
---
## update-id-statement
```clojure
(update-id-statement table from to)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L66-L67)
---
## multi-insert-statement
```clojure
(multi-insert-statement table batch-size)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L69-L77)
---
## copy-row-statement
```clojure
(copy-row-statement table to from)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L79-L83)
---
## delete-store-statement
```clojure
(delete-store-statement table)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L85-L86)
---
## change-row-id
```clojure
(change-row-id db table from to)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L88-L92)
---
## read-all
```clojure
(read-all db table id)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L94-L101)
---
## SQLiteRow
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L108-L150)
---
## ->SQLiteRow
```clojure
(->SQLiteRow db key data cache)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L108-L150)
---
## map->SQLiteRow
```clojure
(map->SQLiteRow m)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L108-L150)
---
## SQLiteTable
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L189-L245)
---
## ->SQLiteTable
```clojure
(->SQLiteTable db-spec db table)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L189-L245)
---
## map->SQLiteTable
```clojure
(map->SQLiteTable m)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L189-L245)
---
## prepare-spec
```clojure
(prepare-spec db-spec opts-table)
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L247-L250)
---
## connect-store
```clojure
(connect-store db-spec & {:keys [opts] :as params})
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L252-L269)
---
## release
```clojure
(release store env)
```
Closes the SQLite database connection pools.
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L271-L278)
---
## delete-store
```clojure
(delete-store db-spec & {:keys [table opts]})
```
[source,window=_blank](https://github.com/outskirtslabs/datahike-sqlite/blob/main/src/datahike_sqlite/konserve.clj#L280-L285)
## Datahike SQLite Backend
# Datahike SQLite Backend
> SQLite storage backend for Datahike, the open datalog database.


This library provides a backend for
[Datahike](https://github.com/replikativ/datahike) using
[SQLite](https://www.sqlite.org) as the backing store, with direct native
integration through
[sqlite4clj](https://github.com/andersmurphy/sqlite4clj).
> ++[++!WARNING++]++ This project is highly experimental and not
> production ready.
Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**.
## Why sqlite4clj?
Unlike the [datahike-jdbc](https://github.com/replikativ/datahike-jdbc/)
backend, this implementation uses
[sqlite4clj](https://github.com/andersmurphy/sqlite4clj) - a minimalist
FFI binding to SQLite’s C API using Java 22’s Foreign Function Interface
(Project Panama). This approach offers several advantages:
* Bypasses JDBC overhead by interfacing directly with SQLite’s C API
through FFI for direct SQLite access.
* SQLite’s embedded nature doesn’t require thread-backed connection
pools like c3p0/HikariCP, eliminating that complexity.
* Provides better performance through cached prepared statements per
connection and inline caching of column reading functions.
* Eliminates dependencies on sqlite-jdbc, c3p0, and next.jdbc for a
smaller footprint.
* Easier access to SQLite-specific features and pragmas for targeted
optimizations.
* More suitable architecture for SQLite’s single-writer, multiple-reader
model.
## Usage
Include the library in your deps.edn:
```clojure
io.replikativ/datahike {:mvn/version "0.6.1601"} ;; Use latest version
ramblurr/datahike-sqlite {:git/url "https://github.com/ramblurr/datahike-sqlite"
:git/sha "c94e449be351b13c7b279d39ee3266cc22dd8f7d"}
```
### Dependencies
* The https://clojars.org/io.replikativ/datahike[io.replikativ/datahike
dependency] must be provided by your project and this backend is
designed to work with Datahike 0.6.x versions.
* [sqlite4clj](https://github.com/andersmurphy/sqlite4clj) requires Java
22 or later.
* If you use this library as a git dependency, you will need to prepare
the library with `clj -X:deps prep`.
* You must include
`:jvm-opts ++[++"--enable-native-access=ALL-UNNAMED"++]++` in your
deps.edn alias.
* When creating an executable jar file, you can avoid the need to pass
this argument by adding the manifest attribute
`Enable-Native-Access: ALL-UNNAMED` to your jar.
### Example
```clojure
(require
'[datahike-sqlite.core] ;; required to pull in the multi-method implementations
'[datahike.api :as d])
(def cfg {:store {:backend :sqlite
:dbname "foobar.sqlite"
;; see sqlite4clj.core/init-db! for the possible options
:sqlite-opts {:pool-size 4}}})
(d/database-exists? cfg)
;; => false
(d/create-database cfg)
(def conn (d/connect cfg))
(d/transact conn [{:db/ident :artifact
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :level
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one}])
(d/transact conn [{:name "Mighty Teapot" :age 20}])
(d/q '[:find (pull ?e [*])
:in $ ?name
:where [?e :name ?name]]
@conn "Mighty Teapot")
;; this will delete the table in the sqlite file,
;; but will not delete the sqlite file itself
(d/delete-database cfg)
(d/release conn)
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/datahike-sqlite/next/)
* [API Reference](https://docs.outskirtslabs.com/datahike-sqlite/next/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/datahike-sqlite/issues)
## Configuration
The value for `:store` is a configuration map. To invoke datahike-sqlite
(this library) you must include `:backend :sqlite` in that map.
You must also include `:dbname`, a path to the SQLite file.
You can optionally include the key `:sqlite-opts` with an options map
which will be passed to
[`sqlite4clj.core/init-db!`](https://github.com/andersmurphy/sqlite4clj).
## Development
### Testing
```bash
bb test
```
### Formatting
```shell
bb fmt
```
### Linting
```shell
bb lint
```
## License: MIT License
Copyright © 2025 Casey Link casey@outskirtslabs.com Distributed under
the [MIT](https://spdx.org/licenses/MIT.html).
## starfederation.datastar.clojure.expressions
# starfederation.datastar.clojure.expressions
## ->expr
```clojure
(->expr & forms)
```
Compiles a clojure form into a datastar expression.
TODO docs
_macro_
[source,window=_blank](https://github.com/outskirtslabs/datastar-expressions/blob/main/src/starfederation/datastar/clojure/expressions.clj#L8-L15)
## datastar-expressions
# datastar-expressions
> Clojure to Datastar expression transpiler


`expressions` is a proof-of-concept for writing https://data-star.dev[🚀
datastar] expressions using Clojure without manual string concatenation.
Instead of:
```clojure
[:button {:data-on-click (str "$person-id" (:id person) " @post('/update-person')")}]`
```
Write this:
```clojure
[:button {:data-on-click (->expr
(set! $person-id ~(:id person))
(@post "/update-person"))}]
```
It is powered by [squint](https://github.com/squint-cljs/squint), thanks
[@borkdude](https://github.com/borkdude).
Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**.
## Goal & Non-Goals
Since Clojure does not have string interpolation, writing even simple
https://data-star.dev/guide/datastar_expressions[Datastar (d++*++)
expressions] can involve a lot of `str` or `format` gymnastics.
The goal of `expressions` is to add a little bit of syntax sugar when
writing d++*++ expressions so that they can be read and manipulated as
s-expressions.
D++*++ expressions are not exactly javascript, though they are
interpreted by the js runtime. D++*++ expressions also do not have a
grammar or really any formal definition of any kind. Delaney’s official
position is that the simplest and obvious expressions a human would
write should work.
`expressions` follows that by trying to provide coverage for 99% of
simple and obvious expressions.
⚠️ You can totally write expressions that result in broken javascript,
that is not necessarily a bug.
## Install
```clojure
datastar/expressions {:git/url "https://github.com/outskirtslabs/datastar-expressions/"
:git/sha "53efc7093be1ba33b331b4a27884be8925d8bdce"}
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/datastar-expressions/next/)
* [API Reference](https://docs.outskirtslabs.com/datastar-expressions/next/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/datastar-expressions/issues)
## Status
`expressions` is experimental and breaking changes will occur as it is
actively being developed. Please share your feedback so we can squash
bugs and arrive at a stable release.
## REPL Exploration
To see what this is all about, you can clone this repo and play with the
demos:
```
clojure -M:dev ;; (bring your own repl server)
```
Check out [`dev/user.clj`](./dev/user.clj) and
[`dev/demo.clj`](./dev/demo.clj)
## Example Usage
```clojure
(ns user
(:require [starfederation.datastar.clojure.expressions :refer [->expr]]))
;; Samples
(def record {:record-id "1234"})
;; You have to unquote (~) forms you want evaluated
;; Otherwise no quoting is needed!
;; vars and locals are available for evaluation
(let [val 42]
(->expr
(set! $forty-two ~val)))
;; => "$forty-two = 42;"
(let [val (random-uuid)]
(->expr
(set! $forty-two ~(str val))))
;; => "$forty-two = \"745a9225-890f-41a7-9fc4-008770a68e7e\";"
;; kebab case preservation
(->expr
(set! $record-id ~(:record-id record)))
;; => "$record-id = \"1234\";"
;; actually... all case preservation :)
(->expr
(set! $record_id ~(:record-id record)))
;; => "$record_id = \"1234\";"
(->expr
(set! $recordId ~(:record-id record)))
;; => "$recordId = \"1234\";"
;; namespaced signals work of course
(->expr
(set! $person.first-name "alice"))
;; => "$person.first-name = \"alice\";"
;; primitive functions work too (squint adds parens, but its ok)
(let [val 1]
(->expr
(set! $forty-two (+ ~val $forty-one))))
;; => "$forty-two = (1) + ($forty-one);"
;; calling js functions:
(->expr (pokeBear $bear-id))
;; => "pokeBear($bear-id)"
;; actions
(->expr (@get "/poke"))
;; => "@get(\"/poke\")"
(->expr (@patch "/poke"))
;; => "@patch(\"/poke\")"
;; expr with multiple statements are in order like you would expect
(->expr
(set! $bear-id 1234)
(pokeBear $bear-id)
(@post "/bear-poked"))
;; => "$bear-id = 1234;; pokeBear($bear-id); @post(\"/bear-poked\")"
;; You can build dynamic signal names by using the $signal in the first position
(let [field-name "name"]
(->expr
(set! ($bear. ~field-name) "Yogi")
(@post "/bear")))
;; => "$bear.name = \"Yogi\";; @post(\"/bear\")"
;; logical conjunctions and disjunctions
(->expr (and (= $my-signal "bar")
"ret-val"))
;; => "(($my-signal) === (\"bar\")) && (\"ret-val\")"
;; But you should probably use when/if
(->expr (when (= $my-signal "bar")
"ret-val"))
;; => "((($my-signal) === (\"bar\")) ? ((\"ret-val\")) : (null))"
(->expr (if (= $my-signal "bar")
"true-val"
"false-val"))
;; => "((($my-signal) === (\"bar\")) ? (\"true-val\") : (\"false-val\"))"
;; A few other variations
(->expr (&& (or (= evt.key "Enter")
(&& evt.ctrlKey (= evt.key "1")))
(alert "Key Pressed")))
;; => "(((evt.key) === (\"Enter\")) || ((evt.ctrlKey) && ((evt.key) === (\"1\")))) && (alert(\"Key Pressed\"))"
;; This one is interesting, see how it uses the , operator to separate sub-expressions
(->expr (when (= evt.key "Enter")
(evt.preventDefault)
(alert "Key Pressed")))
;; => "(((evt.key) === (\"Enter\")) ? ((evt.preventDefault()), (alert(\"Key Pressed\"))) : (null))"
;; And here is one for data-class
(->expr {"hidden" (&& $fetching-bears
(= $bear-id 1))})
;; => "({ \"hidden\": ($fetching-bears) && (($bear-id) === (1)) })"
;; It also does edn->json conversion, so setting initial signals is possible
(->expr {:my-signal "init-value"})
;; => "({ \"my-signal\": \"init-value\" })"
(->expr
(let [value $my-signal]
(println value)
(and (= $my-signal "bear")
(@post "/foo"))))
;; => "(() => { const value1 = $my-signal; console.log((value1)); return (($my-signal) === (\"bear\")) && (@post(\"/foo\")); })()"
;; JS template strings are supported
;; Since ` is used by the reader, we just wrap the whole thing in quotes
(->expr
(@post ("`/ping/${evt.srcElement.id}`")))
;; => "@post(`/ping/${evt.srcElement.id}`)"
;; Negation
(->expr (not $foo))
;; => "(!($foo))"
(->expr (not (= 1 2)))
;; => "(!((1) === (2)))"
(->expr (not= (+ 1 3) 4))
;; => "((1) + (3)) !== (4)"
(->expr (set! $ui._leftnavOpen (not $ui._leftnavOpen)))
;; => "$ui._leftnavOpen = (!($ui._leftnavOpen))"
;; if
(->expr (set! $ui._leftnavOpen (if $ui._leftnavOpen false true)))
;; => "$ui._leftnavOpen = (($ui._leftnavOpen) ? (false) : (true))"
(->expr (if $ui._leftnavOpen
(set! $ui._leftnavOpen false)
(set! $ui._leftnavOpen true)))
;; => "(($ui._leftnavOpen) ? ($ui._leftnavOpen = false) : ($ui._leftnavOpen = true))"
;; expr/raw is an escape hatch to emit raw JS
;; raw/1 emits its argument as is
(->expr (set! $foo (expr/raw "!$foo")))
;; => "$foo = !$foo"
(let [we-are "/back-in-string-concat-land"]
(->expr
(set! $volume 11)
(expr/raw ~(str "window.location = " we-are))))
;; => "$volume = 11; window.location = /back-in-string-concat-land"
;; raw/0 emits nothing
(->expr (set! $foo (expr/raw)))
;; => "$foo ="
;; bare symbols
(->expr $ui._mainMenuOpen)
;; => "$ui._mainMenuOpen"
;; when-not
(->expr (when-not (= 1 1)
(set! $ui._mainMenuOpen true)))
;; => "(((1) === (1)) ? (null) : ($ui._mainMenuOpen = true))"
;; bare booleans
(->expr (when false
(set! $foo true)))
;; => "((!!(false)) ? (($foo = true)) : (null))"
```
## Known Limitations
```clojure
;; a generated symbol (el-id below) cannot be used in a template string
(->expr (let [el-id evt.srcElement.id]
(when el-id
(@post ("`/ping/${el-id}`")))))
;; => "(() => { const el_id1 = evt.srcElement.id; if (el_id1) { return @post(`/ping/${el-id}`)} else { return alert(\"No id\")}; })()"
;; No condp
(->expr (condp = $ui._mainMenuOpen
true (set! $ui._mainMenuOpen false)
false (set! $ui._mainMenuOpen true)))
;; => "(() => { const pred__288831 = squint_core._EQ_; const expr__288842 = $ui._mainMenuOpen; if (!!(pred__288831(true, expr__288842))) { return $ui._mainMenuOpen = false} else { if (!!(pred__288831(false, expr__288842))) { return $ui._mainMenuOpen = true} else { throw new java.lang.IllegalArgumentException(squint_core.str(\"No matching clause: \", expr__288842))}}; })()"
```
## License: MIT License
Copyright © 2025 Casey Link casey@outskirtslabs.com
Distributed under the [MIT](https://spdx.org/licenses/MIT.html).
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## [UNRELEASED]
## v0.12.0 (2026-02-03)
This is a version bump release:
* Added package versions for
[version 1.0.7491](https://docs.datomic.com/changes/pro.html#1.0.7491)
`pkgs.datomic-pro` will always be the latest release, but the following
specific versions are also available:
* `pkgs.datomic-pro_1_0_7491` (latest)
* `pkgs.datomic-pro_1_0_7482`
* `pkgs.datomic-pro_1_0_7469`
* `pkgs.datomic-pro_1_0_7394`
* `pkgs.datomic-pro_1_0_7387`
* `pkgs.datomic-pro_1_0_7364`
* `pkgs.datomic-pro_1_0_7277`
And for peer:
* `pkgs.datomic-pro-peer_1_0_7491` (latest)
* `pkgs.datomic-pro-peer_1_0_7482`
* `pkgs.datomic-pro-peer_1_0_7469`
* `pkgs.datomic-pro-peer_1_0_7394`
* `pkgs.datomic-pro-peer_1_0_7387`
* `pkgs.datomic-pro-peer_1_0_7364`
* `pkgs.datomic-pro-peer_1_0_7277`
## v0.11.0 (2026-02-03)
### Breaking
* NixOS module: The `services.datomic-pro.package` option is now
required. You must explicitly pin your Datomic version. This change
prevents unexpected upgrades that could affect your data.
* Switch from [SemVer](https://semver.org/spec/v2.0.0.html) to
[BreakVer](https://www.taoensso.com/break-versioning). I actually had
meant to use BreakVer all along and thought we were, but didn’t see the
copy paste error until just now (yes this is a violation of SemVer by
not bumping the major version number).
### Added
* Add automatic version bumping. Thanks to @licht1stein for
contributing.
## v0.10.0 (2026-02-03)
This is a version bump release:
* Added package versions for
[version 1.0.7482](https://docs.datomic.com/changes/pro.html#1.0.7482)
`pkgs.datomic-pro` will always be the latest release, but the following
specific versions are also available:
* `pkgs.datomic-pro_1_0_7482` (latest)
* `pkgs.datomic-pro_1_0_7469`
* `pkgs.datomic-pro_1_0_7394`
* `pkgs.datomic-pro_1_0_7387`
* `pkgs.datomic-pro_1_0_7364`
* `pkgs.datomic-pro_1_0_7277`
And for peer:
* `pkgs.datomic-pro-peer_1_0_7482` (latest)
* `pkgs.datomic-pro-peer_1_0_7469`
* `pkgs.datomic-pro-peer_1_0_7394`
* `pkgs.datomic-pro-peer_1_0_7387`
* `pkgs.datomic-pro-peer_1_0_7364`
* `pkgs.datomic-pro-peer_1_0_7277`
## v0.9.0 (2025-12-14)
This is a version bump release:
* Added package versions for
[version 1.0.7469](https://docs.datomic.com/changes/pro.html#1.0.7469)
`pkgs.datomic-pro` will always be the latest release, but the following
specific versions are also available:
* `pkgs.datomic-pro_1_0_7469` (latest)
* `pkgs.datomic-pro_1_0_7394`
* `pkgs.datomic-pro_1_0_7387`
* `pkgs.datomic-pro_1_0_7364`
* `pkgs.datomic-pro_1_0_7277`
And for peer:
* `pkgs.datomic-pro-peer_1_0_7469` (latest)
* `pkgs.datomic-pro-peer_1_0_7394`
* `pkgs.datomic-pro-peer_1_0_7387`
* `pkgs.datomic-pro-peer_1_0_7364`
* `pkgs.datomic-pro-peer_1_0_7277`
### New Contributors
* @licht1stein made their first contribution in
https://github.com/outskirtslabs/datomic-pro-flake/pull/6
## v0.8.0 (2025-09-08)
The project repository has moved from my personal github to my
open-source project org:
* from https://github.com/Ramblurr/datomic-pro-flake
* to https://github.com/outskirtslabs/datomic-pro-flake/
This is also a version bump release:
* Added package versions for
[version 1.0.7394](https://docs.datomic.com/changes/pro.html#1.0.7394)
`pkgs.datomic-pro` will always be the latest release, but the following
specific versions are also available:
* `pkgs.datomic-pro_1_0_7394` (latest)
* `pkgs.datomic-pro_1_0_7387`
* `pkgs.datomic-pro_1_0_7364`
* `pkgs.datomic-pro_1_0_7277`
And for peer:
* `pkgs.datomic-pro-peer_1_0_7394` (latest)
* `pkgs.datomic-pro-peer_1_0_7387`
* `pkgs.datomic-pro-peer_1_0_7364`
* `pkgs.datomic-pro-peer_1_0_7277`
## v0.7.0 (2025-07-09)
This is a version bump release:
* Added package versions for
[version 1.0.7387](https://docs.datomic.com/changes/pro.html#1.0.7387)
`pkgs.datomic-pro` will always be the latest release, but the following
specific versions are also available:
* `pkgs.datomic-pro_1_0_7387` (latest)
* `pkgs.datomic-pro_1_0_7364`
* `pkgs.datomic-pro_1_0_7277`
And for peer:
* `pkgs.datomic-pro-peer_1_0_7387` (latest)
* `pkgs.datomic-pro-peer_1_0_7364`
* `pkgs.datomic-pro-peer_1_0_7277`
## v0.6.1 (2025-05-15)
No changes, just ci wrangling.
## v0.6.0 (2025-05-15)
This release brings versioned packages! We recommend you pin your
deployments to specific versions and upgrade intentionally.
### Changed
Package versions:
`pkgs.datomic-pro` will always be the latest release, but the following
specific versions are also available:
* `pkgs.datomic-pro_1_0_7364` (latest)
* `pkgs.datomic-pro_1_0_7277`
And for peer:
* `pkgs.datomic-pro-peer_1_0_7364` (latest)
* `pkgs.datomic-pro-peer_1_0_7277`
## v0.5.0 (2025-05-15)
This release brings a Datomic version bump to
[version 1.0.7364](https://docs.datomic.com/changes/pro.html#1.0.7364).
Also notably the container image size is now 433M, down from over 750M,
thanks to Datomic’s efforts to slim down the release jar!
### Changed
* nix pkg: Updated `datomic-pro` and `datomic-pro-peer` to
[version 1.0.7364](https://docs.datomic.com/changes/pro.html#1.0.7364)
* docs: Improved SQLite example with rails 8 inspired tuning
## v0.4.0 (2025-03-14)
### Changed
* nix pkg: Updated `datomic-pro` and `datomic-pro-peer` to version
1.0.7277
## v0.3.0 (2024-11-01)
Nothing changed in 0.3.0, I just am struggling with
[flakehub’s](https://flakehub.com/flake/ramblurr/datomic-pro?view=releases)
release process.
## v0.2.0 (2024-11-01)
### Breaking
* nix pkg: `transactor` bin renamed to `datomic-transactor`
* nix pkg: `console` bin renamed to `datomic-console`
* nixos module: removed the default settings that leaned towards dev/h2
storage by default
### Added
* oci image: Added Docker container image with lots of customizable
features
* Includes sqlite, postgresql, and mysql JDBC drivers by default
* Ability to customize the CLASSPATH and LD_LIBRARY_PATH
* `unstable` container image tag that follows the `main` branch
* nix pkg: Added ability to override the build and add extra native libs
or java libs
* nix pkg: Exposed more packages: `datomic-shell`, `datomic-run`,
`datomic-repl`, `datomic-peer-server`
* nixos module: You can now configure: logging, extra classpath entries,
and extra java options.
* nix pkg: Added datomic-pro-peer package which is the datomic peer
library with all of its dependencies
* nix pkg: Added option to build slimmed down JRE for datomic-pro
### Changed
* nix pkg: Updated datomic-pro to version 1.0.7260
* nix pkg: Switched to Nix’s JDK 21 headless package (which is supported
by Datomic)
* oci image: Use the slimmed down JRE and a custom babashka build to
reduce size of the image
### Fixed
* This changelog formatting
## v0.1.0 (2024-06-12)
### Added
* Created this flake with datomic-pro version 1.0.7075
## Docker/OCI Container
# Docker/OCI Container
`datomic-pro-flake` publishes an OCI image that can run Datomic Pro transactor or Datomic Console.
* Image: `ghcr.io/outskirtslabs/datomic-pro`
* Default command: transactor
* Console command: `console`
If you do not want to build with nix, pull a published image:
```shell
docker pull ghcr.io/outskirtslabs/datomic-pro:1.0.7491
```
Package tags are listed at:
https://github.com/orgs/outskirtslabs/packages/container/package/datomic-pro
## Transactor Mode
Transactor mode runs when no command is provided.
* Default port: `4334`
* Required rw volume: `/config`
* Optional rw volume: `/data` (for local/H2 or sqlite-style setups)
* PostgreSQL, MySQL, and SQLite JDBC drivers are included
You can provide `/config/transactor.properties` directly, or configure via env vars.
### Supported Environment Variables
- ❗ IMPORTANT
-
Every supported variable can also be passed with `_FILE` to load the value from a file.
Example: `DATOMIC_STORAGE_ADMIN_PASSWORD_FILE=/run/secrets/admin-password`.
* `DATOMIC_TRANSACTOR_PROPERTIES_PATH` (`/config/transactor.properties`)
* `DATOMIC_ALT_HOST` (`alt-host`)
* `DATOMIC_DATA_DIR` (`data-dir`, default `/data`)
* `DATOMIC_ENCRYPT_CHANNEL` (`encrypt-channel`)
* `DATOMIC_HEARTBEAT_INTERVAL_MSEC` (`heartbeat-interval-msec`)
* `DATOMIC_HOST` (`host`, default `0.0.0.0`)
* `DATOMIC_MEMCACHED` (`memcached`)
* `DATOMIC_MEMCACHED_AUTO_DISCOVERY` (`memcached-auto-discovery`)
* `DATOMIC_MEMCACHED_CONFIG_TIMEOUT_MSEC` (`memcached-config-timeout-msec`)
* `DATOMIC_MEMCACHED_PASSWORD` (`memcached-password`)
* `DATOMIC_MEMCACHED_USERNAME` (`memcached-username`)
* `DATOMIC_MEMORY_INDEX_MAX` (`memory-index-max`, default `256m`)
* `DATOMIC_MEMORY_INDEX_THRESHOLD` (`memory-index-threshold`, default `32m`)
* `DATOMIC_OBJECT_CACHE_MAX` (`object-cache-max`, default `128m`)
* `DATOMIC_PID_FILE` (`pid-file`)
* `DATOMIC_HEALTHCHECK_CONCURRENCY` (`ping-concurrency`)
* `DATOMIC_HEALTHCHECK_HOST` (`ping-host`)
* `DATOMIC_HEALTHCHECK_PORT` (`ping-port`)
* `DATOMIC_PORT` (`port`, default `4334`)
* `DATOMIC_PROTOCOL` (`protocol`, default `dev`)
* `DATOMIC_READ_CONCURRENCY` (`read-concurrency`)
* `DATOMIC_SQL_DRIVER_CLASS` (`sql-driver-class`)
* `DATOMIC_SQL_URL` (`sql-url`)
* `DATOMIC_STORAGE_ACCESS` (`storage-access`, default `remote`)
* `DATOMIC_STORAGE_ADMIN_PASSWORD` (`storage-admin-password`)
* `DATOMIC_STORAGE_DATOMIC_PASSWORD` (`storage-datomic-password`)
* `DATOMIC_VALCACHE_MAX_GB` (`valcache-max-gb`)
* `DATOMIC_VALCACHE_PATH` (`valcache-path`)
* `DATOMIC_WRITE_CONCURRENCY` (`write-concurrency`)
To disable env-to-properties generation and use your own full config file, set:
* `DOCKER_DATOMIC_GENERATE_PROPERTIES_SKIP` to any non-empty value
## Console Mode
Run with `console` as the first argument.
* Default port: `8080`
* `DB_URI` sets the connection URI
* `DB_URI_FILE` loads the URI from a file
## Example Compose
### Datomic Pro With Local Storage
```yaml
---
services:
datomic-transactor:
image: ghcr.io/outskirtslabs/datomic-pro:1.0.7491
environment:
DATOMIC_STORAGE_ADMIN_PASSWORD: unsafe
DATOMIC_STORAGE_DATOMIC_PASSWORD: unsafe
volumes:
- ./data:/data
ports:
- 127.0.0.1:4334:4334
datomic-console:
image: ghcr.io/outskirtslabs/datomic-pro:1.0.7491
command: console
environment:
DB_URI: datomic:dev://datomic-transactor:4334/?password=unsafe
ports:
- 127.0.0.1:8081:8080
```
### Datomic Pro With SQLite Storage
Prepare the sqlite database first:
```shell
mkdir -p data/ config/
sqlite3 data/datomic-sqlite.db '
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA mmap_size = 134217728;
PRAGMA journal_size_limit = 67108864;
PRAGMA cache_size = 2000;
CREATE TABLE datomic_kvs (
id TEXT NOT NULL,
rev INTEGER,
map TEXT,
val BYTEA,
CONSTRAINT pk_id PRIMARY KEY (id)
);'
```
Then run compose:
```yaml
---
services:
datomic-transactor:
image: ghcr.io/outskirtslabs/datomic-pro:unstable
environment:
DATOMIC_PROTOCOL: sql
DATOMIC_SQL_URL: jdbc:sqlite:/data/datomic-sqlite.db
DATOMIC_SQL_DRIVER_CLASS: org.sqlite.JDBC
DATOMIC_JAVA_OPTS: -Dlogback.configurationFile=/config/logback.xml
DATOMIC_HOST: datomic-transactor
DATOMIC_ALT_HOST: "127.0.0.1"
volumes:
- "./data:/data:z"
- "./config:/config:z"
ports:
- 127.0.0.1:4334:4334
datomic-console:
image: ghcr.io/outskirtslabs/datomic-pro:unstable
command: console
environment:
DB_URI: "datomic:sql://?jdbc:sqlite:/data/datomic-sqlite.db"
volumes:
- "./data:/data:z"
ports:
- 127.0.0.1:8081:8080
```
### Datomic Pro With PostgreSQL And Memcached
```yaml
---
services:
datomic-memcached:
image: docker.io/memcached:latest
command: memcached -m 1024
ports:
- 127.0.0.1:11211:11211
restart: always
datomic-storage:
image: docker.io/library/postgres:latest
environment:
POSTGRES_PASSWORD: unsafe
command: postgres -c 'max_connections=1024'
volumes:
- ./data:/var/lib/postgresql/data
ports:
- 127.0.0.1:5432:5432
restart: always
datomic-storage-migrator:
image: ghcr.io/outskirtslabs/datomic-pro:1.0.7491
environment:
PGUSER: postgres
PGPASSWORD: unsafe
volumes:
- "./postgres-migrations:/migrations"
entrypoint: /bin/sh
command: >
-c '(psql -h datomic-storage -lqt | cut -d \| -f 1 | grep -qw "datomic" || psql -h datomic-storage -f /opt/datomic-pro/bin/sql/postgres-db.sql) &&
(psql -h datomic-storage -d datomic -c "\\dt" | grep -q "datomic_kvs" || psql -h datomic-storage -d datomic -f /opt/datomic-pro/bin/sql/postgres-table.sql) &&
(psql -h datomic-storage -d datomic -c "\\du" | cut -d \| -f 1 | grep -qw "datomic" || psql -h datomic-storage -d datomic -f /opt/datomic-pro/bin/sql/postgres-user.sql)'
datomic-transactor:
image: ghcr.io/outskirtslabs/datomic-pro:1.0.7491
environment:
DATOMIC_STORAGE_ADMIN_PASSWORD: unsafe
DATOMIC_STORAGE_DATOMIC_PASSWORD: unsafe
DATOMIC_PROTOCOL: sql
DATOMIC_SQL_URL: jdbc:postgresql://datomic-storage:5432/datomic?user=datomic&password=datomic
DATOMIC_HEALTHCHECK_HOST: 127.0.0.1
DATOMIC_HEALTHCHECK_PORT: 9999
DATOMIC_MEMCACHED: datomic-memcached:11211
ports:
- 127.0.0.1:4334:4334
restart: always
datomic-console:
image: ghcr.io/outskirtslabs/datomic-pro:1.0.7491
command: console
environment:
DB_URI: datomic:sql://?jdbc:postgresql://datomic-storage:5432/datomic?user=datomic&password=datomic
ports:
- 127.0.0.1:8081:8080
```
## Discussion
If something is missing for your deployment style, open an issue:
https://github.com/outskirtslabs/datomic-pro-flake/issues/new
## datomic-pro-flake
# datomic-pro-flake
> Datomic Pro packaged for Nix w/ NixOS modules and OCI container images.


[(https://img.shields.io/github/license/outskirtslabs/datomic-pro-flake)]
`datomic-pro-flake` provides:
* Versioned `datomic-pro` and `datomic-pro-peer` nix packages.
* NixOS modules for running Datomic Pro transactor and Datomic Console.
* An OCI image (no nix requiredd!) for running transactor or console with environment variables and `_FILE` secrets.
All outputs are tested end-to-end in this repository.
Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**.
## Quick Start
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
datomic-pro.url = "github:outskirtslabs/datomic-pro-flake";
datomic-pro.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, datomic-pro, ... }:
let
system = "x86_64-linux";
in {
nixosConfigurations.example = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./configuration.nix
datomic-pro.nixosModules.${system}.datomic-pro
];
};
};
}
```
## Documentation
To learn how to configure the module and OCI container, see:
* [NixOS Module](nixos-module.adoc)
* [Docker/OCI Container](docker-oci-container.adoc)
* [Docs Home](https://docs.outskirtslabs.com/datomic-pro-flake/next/)
* [Support via GitHub Issues](https://github.com/outskirtslabs/datomic-pro-flake/issues)
## Security
See [Security](security.adoc) for security reporting and policy links.
## License
Copyright © 2025-2026 Casey Link <casey@outskirtslabs.com>
Distributed under the [Apache License 2.0](https://spdx.org/licenses/Apache-2.0.html).
## NixOS Module
# NixOS Module
This flake provides NixOS modules for Datomic Pro transactor and Datomic Console.
## Add The Module
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
datomic-pro.url = "github:outskirtslabs/datomic-pro-flake";
datomic-pro.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, datomic-pro, ... }:
let
system = "x86_64-linux";
in {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./configuration.nix
datomic-pro.nixosModules.${system}.datomic-pro
datomic-pro.nixosModules.${system}.datomic-console
];
};
};
}
```
## Secrets File
Create `/etc/datomic-pro/secrets.properties`:
```properties
storage-admin-password=changeme
storage-datomic-password=changeme
```
- ❗ IMPORTANT
-
Do not use `environment.etc` for secrets. That writes data to the nix store, which is globally readable and can be cached remotely.
Lock down permissions:
```shell
chown root:root /etc/datomic-pro/secrets.properties
chmod 0600 /etc/datomic-pro/secrets.properties
```
## Example Configuration
A basic dev-mode transactor storing data under `/var/lib/datomic-pro`:
```nix
{
services.datomic-pro = {
enable = true;
package = pkgs.datomic-pro_1_0_7491;
secretsFile = "/etc/datomic-pro/secrets.properties";
settings = {
host = "localhost";
port = 4334;
memory-index-max = "256m";
memory-index-threshold = "32m";
object-cache-max = "128m";
protocol = "dev";
storage-access = "remote";
};
};
services.datomic-console = {
enable = true;
port = 8080;
dbUriFile = "/etc/datomic-pro/console-db-uri";
};
}
```
## Available Packages
`pkgs.datomic-pro` and `pkgs.datomic-pro-peer` track the latest supported Datomic release.
Specific versions are also exposed, for example:
* `pkgs.datomic-pro_1_0_7491`
* `pkgs.datomic-pro_1_0_7482`
* `pkgs.datomic-pro_1_0_7469`
* `pkgs.datomic-pro-peer_1_0_7491`
* `pkgs.datomic-pro-peer_1_0_7482`
* `pkgs.datomic-pro-peer_1_0_7469`
New upstream Datomic releases are typically added within 24 hours.
## Security
# Security
Security issues should be reported privately.
Please review the Outskirts Labs policy first:
[Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy)
For this repository specifically:
* [Security advisories](https://github.com/outskirtslabs/datomic-pro-flake/security/advisories)
* [Report a vulnerability](https://github.com/outskirtslabs/datomic-pro-flake/security/advisories/new)
## h2o-zig
# h2o-zig
> libh2o packaged for Zig with cross-compilation support for Linux and
> macOS.
[(https://img.shields.io/badge/doc-outskirtslabs-orange.svg)]
https://docs.outskirtslabs.com/open-source-vital-signs#maturing[image:https://img.shields.io/badge/status-maturing-blue.svg[status:
maturing]]

This is [libh2o](https://h2o.examp1e.net/), packaged for Zig with
cross-compilation support for Linux and macos.
* all dependencies are statically linked into the output
* output is a static library (`.a`) for embedding into other projects
The intended usage is for building language bindings and FFI wrappers
that need to expose all h2o and SSL symbols in a final shared library.
Included h2o features:
* http 1
* http 2
* picotls
* quic {plus} http3
* brotli
* zstd
* [libaegis](https://github.com/aegis-aead/libaegis) for
[draft-irtf-cfrg-aegis-aead-18](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aegis-aead/)
features explicitly excluded:
* mruby
* memcached integration
* redis integration
* libuv
Supported targets:
* `x86++_++64-linux`
* `aarch64-linux`
* `x86++_++64-macos`
* `aarch64-macos`
Project status:
**[Maturing](https://docs.outskirtslabs.com/open-source-vital-signs#maturing)**.
## Quick start
1. Install [zig](https://ziglang.org/)
2. `zig build` or `zig build -Dtarget=++<++target++>++` (where
`+<++target++>+` is from the above list)
## Documentation
* [Docs](https://docs.outskirtslabs.com/h2o-zig/next/)
* [API Reference](https://docs.outskirtslabs.com/h2o-zig/next/api)
* https://github.com/outskirtslabs/h2o-zig/issues[Support via GitHub
Issues]
## Prerequisites
You need the following installed:
* [Zig](https://ziglang.org/) 0.15.2
* Perl (for an h2o build step)
If you have nix you can use the dev shell provided by the flake in this
repo.
## Use as a dependency
First, update your `build.zig.zon`:
```
zig init # if you don't have a build.zig already
zig fetch --save git+https://github.com/outskirtslabs/h2o-zig.git
```
You can then import `h2o` in your `build.zig` with:
```zig
const h2o_dependency = b.dependency("h2o", .{
.target = target,
.optimize = optimize,
});
your_exe.linkLibrary(h2o_dependency.artifact("h2o-evloop"));
```
### Build Options
The following build-time flags are available:
* `-Duse-boringssl=++<++bool++>++` - Use BoringSSL instead of OpenSSL
(default: `true`)
* `-Duse-external-brotli=++<++bool++>++` - Use external brotli
dependency instead of vendored sources (default: `true`). The vendored
one is from upstream h2o and is a much older version than the external
one.
Example usage:
```zig
const h2o_dependency = b.dependency("h2o", .{
.target = target,
.optimize = optimize,
.@"use-boringssl" = true, // default
});
your_exe.linkLibrary(h2o_dependency.artifact("h2o-evloop"));
```
Or via command line when building:
```bash
zig build -Duse-boringssl=true
```
### Cross-Compilation to macOS
When cross-compiling from Linux to macOS targets (x86++_++64-macos or
aarch64-macos), the build requires the `APPLE++_++SDK++_++PATH`
environment variable to be set. This points to the macOS SDK that
provides system headers and libraries.
**Using Nix (Recommended)**
The provided nix flake automatically sets up `APPLE++_++SDK++_++PATH`
when you enter the development shell:
```bash
nix develop
zig build -Dtarget=aarch64-macos
zig build -Dtarget=x86_64-macos
```
**Manual Setup**
If not using nix, you’ll need to obtain a macOS SDK and set the
environment variable:
```bash
export APPLE_SDK_PATH=/path/to/MacOSX.sdk
zig build -Dtarget=x86_64-macos
```
The SDK must contain `usr/include` with macOS system headers. Without
this, cross-compilation to macOS will fail with an error about the
missing `APPLE++_++SDK++_++PATH` environment variable.
**Note**: Cross-compilation to macOS from macOS does not require
`APPLE++_++SDK++_++PATH` as the system SDK is used automatically.
## Hacking on H2O
This project also serves as a reproducible dev environment for h2o
thanks to the nix flake’s devshell.
Simply activate the nix devshell then:
```bash
git clone https://github.com/h2o/h2o.git h2o
cd h2o
cmake -B build -S . -DDISABLE_LIBUV=ON -DWITH_MRUBY=OFF
cmake --build build -j$(nproc)
cmake --build build --target check
```
### Building picotls (with AEGIS support)
To build and test picotls (the TLS library used by h2o) with AEGIS
cipher support:
```bash
git clone https://github.com/h2o/picotls.git vendor/picotls
cd vendor/picotls
nix develop ../ # enter devshell from picotls directory
cmake -B build -S . \
-DWITH_AEGIS=ON \
-DCMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH \
-DAEGIS_INCLUDE_DIR=$AEGIS_INCLUDE_DIR
cmake --build build -j$(nproc)
./build/test-openssl.t
```
## License: MIT License
h2o-zig is distributed under the
[MIT](https://spdx.org/licenses/MIT.html).
Copyright © 2025 Casey Link casey@outskirtslabs.com
Binary distributions (JAR files on Clojars and GitHub releases) may
bundle the following third-party projects:
* [h2o](https://github.com/h2o/h2o) is licensed under the MIT License and
copyright [DeNA Co., Ltd.](https://dena.com/),
[Kazuho Oku](https://github.com/kazuho/), and contributors.
* [brotli](https://github.com/google/brotli) is licensed under the MIT
License and copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
* [zstd](https://github.com/facebook/zstd) is licensed under the BSD
License and copyright (c) Meta Platforms, Inc.
* [OpenSSL](https://github.com/openssl/openssl) is licensed under the
Apache 2.0 License and copyright (c) 1998-2025 The OpenSSL Project
Authors, and copyright (c) 1995-1998 Eric A. Young, Tim J. Hudson.
* [BoringSSL](https://github.com/google/boringssl) is licensed under the
Apache 2.0 License and copyright
https://github.com/google/boringssl/blob/58da9b0d721fd807279f4e3898741c92cf43bdbd/AUTHORS#[a
bunch of folks]
* [libaegis](https://github.com/aegis-aead/libaegis) is licensed under the
MIT license and copyright (c) 2023-2026 Frank Denis
## Security policy
# Security policy
## Advisories
All security advisories will be posted
https://github.com/outskirtslabs/h2o-zig/security/advisories[on
GitHub].
## Reporting a vulnerability
Please report possible security vulnerabilities
https://github.com/outskirtslabs/h2o-zig/security/advisories[via
GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt
emails with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see
[Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
— [Casey Link](https://casey.link)
## Outskirts Labs Developer Docs
# Outskirts Labs Developer Docs
## Hello
Welcome to Outskirts Labs, where I organize my active and maintained open source projects.
My personal GitHub is cluttered with experiments, forks, and random contributions. This space is for the projects I actively maintain.
I am an independent software consultant, serving clients at the edge of technical capacity, sustainable funding, and mainstream attention.
My work focuses on building tools that last without requiring an army of developers or regular rewrites.
If you're a small business, non-profit, or social enterprise needing custom software that fits both your mission and budget, check out outskirtslabs.com or get in touch directly casey@outskirtslabs.com
p.s. For the curious and fellow Clojurians, my personal homepage at casey.link may be of interest.
## Continuous, rapid, NixOS deployments to Hetzner Cloud
# Continuous, rapid, NixOS deployments to Hetzner Cloud
OpenTofu.
>
> [(https://img.shields.io/badge/doc-outskirtslabs-orange.svg)]
> https://docs.outskirtslabs.com/open-source-vital-signs#static[image:https://img.shields.io/badge/status-static-blue.svg[status:
> static]]
>
> - 📌 NOTE
-
> This repo was based on https://determinate.systems[Determinate
> Systems]'s https://github.com/determinatesystems/demo[repo for AWS
> AMIs]. This is a proof-of-concept repo maintained by me and not DetSys.
> I use something like this in prod, so it works, but don’t expect this
> repo to be updated regularly.
>
>
> This project shows you how to continuously deploy a
> [NixOS](https://zero-to-nix.com/concepts/nixos) configuration to a
> [Hetzner Cloud](https://www.hetzner.com/cloud) server using
> [OpenTofu](https://opentofu.org) and [FlakeHub](https://flakehub.com) in
> seconds.
>
> == Differences from the AWS demo
>
> This repo is a port of the
> [Determinate Systems AWS demo](https://github.com/determinatesystems/demo)
> to Hetzner Cloud. Key differences:
>
> | | | |
> | --- | --- | --- |
> | Aspect | AWS Demo | Hetzner Demo |
> | Demo app | EtherCalc (removed from nixpkgs) | CryptPad |
> | Base image | Pre-built AMIs from Determinate Systems | Custom image built from [nixos-hetzner](https://github.com/outskirtslabs/nixos-hetzner) and uploaded |
> | FlakeHub auth | IAM role (`determinate-nixd login aws`) | API token (`determinate-nixd login token`) |
> | Deployment method | AWS Systems Manager (SSM) | SSH with deploy key |
> | Networking | VPC, subnets, security groups | Simple firewall rules |
> | GitHub auth | OIDC federation with AWS | SSH deploy key as secret |
>
> The core FlakeHub workflow remains the same: build closures in CI,
> publish to FlakeHub Cache, and apply pre-built configurations in
> seconds.
>
> However there are a few caveats:
>
> Since Hetzner Cloud does not have a public marketplace for cloud images,
> you have to build the cloud image yourself (with the help of
> [nixos-hetzner](https://github.com/outskirtslabs/nixos-hetzner)) and
> upload it. Hetzner Cloud API keys are scoped to a project, so you will
> need to upload the image on a per-project basis.
>
> We rely on the tool
> [hcloud-upload-image](https://github.com/apricote/hcloud-upload-image) to
> perform the image creation. It can take awhile. In my experience
> uploading a 4GB image can take 8-12 minutes (assuming a fast pipe and
> bandwidth is not the bottleneck).
>
> If you build the image on a system using FlakeHub Cache, then the cloud
> image will be available to other systems from which you want to deploy
> (that are also using FlakeHub Cache).
>
> Once you have a NixOS image uploaded to HCloud, it is straightforward to
> create a nixos server from the snapshot. You can clickops at
> console.hetzner.com, use the hcloud cli tool, or use terraform/opentofu.
>
> In this demo we use opentofu to provision the server, firewall, ssh key,
> etc.
>
> The deployment process involves fetching a pre-built NixOS
> [closure](https://zero-to-nix.com/concepts/closures) from
> [FlakeHub](https://flakehub.com) and applying it to the Hetzner Cloud
> server, streamlining the deployment process and ensuring consistency
> across deployments.
>
> Timings (YMMV):
>
> | | |
> | --- | --- |
> | Step | Time |
> | Image upload | ~9 minutes |
> | Initial tofu apply | ~4 minutes |
> | - Hetzner resources creation | 3m5s |
> | - FlakeHub nixos provisioning | ~60s |
> | Subsequent deploy (GitHub Actions) | ~3 minutes |
> | - Build {plus} publish | 2m55s |
> | - fh apply via SSH | 14s |
>
> == Sign-up for the FlakeHub beta
>
> To experience this streamlined NixOS deployment pipeline for yourself,
> [sign up for the FlakeHub beta](https://determinate.systems) at
> https://determinate.systems. FlakeHub provides the enterprise-grade Nix
> infrastructure needed to fully use these advanced deployment techniques.
>
> == Prerequisites
>
> * Paid [Hetzner Cloud account](https://www.hetzner.com/cloud) with an API
> token
> * Paid [FlakeHub account](https://flakehub.com) with an API token
> * [Detsys Nix](https://docs.determinate.systems/determinate-nix) with
> flakes enabled
> * [OpenTofu](https://opentofu.org) (available in the dev shell)
> Project status:
> **[Static](https://docs.outskirtslabs.com/open-source-vital-signs#static)**.
>
> == Getting Started
>
> This demo deploys [CryptPad](https://cryptpad.net), a collaborative
> document editing platform, to a Hetzner Cloud server.
>
> Deployment is a two-step process:
>
> 1. [Manual deployment](#manual-deployment) - Run OpenTofu locally to
> create the server infrastructure (required first)
> 2. [Automated deployment](#automated-deployment-with-github-actions) -
> Once the server exists, push to GitHub for CI/CD deployments
> - 💡 TIP
-
> For a full rundown of how everything in the demo works, see
> [How it works](#how-it-works) below.
>
>
> === Manual deployment
>
> ==== 1. Build and upload the base image
>
> First, build and upload a NixOS base image to Hetzner Cloud. This only
> needs to be done once (or when you want to update the base image).
>
> ```shell
> # Enter the dev shell
> nix develop
>
> # Create .env file with your tokens (keeps them out of shell history)
> cat > .env << 'EOF'
> HCLOUD_TOKEN=your-hetzner-token
> FLAKEHUB_TOKEN=your-flakehub-token
> EOF
> chmod 600 .env
>
> # Load and export tokens, then build/upload the image
> set -a && source .env && set +a
> ./scripts/upload-image.sh
>
> # Note the image ID from the output (e.g., 123456789)
> ```
>
> ==== 2. Generate a deploy key
>
> Generate an SSH key for GitHub Actions deployments:
>
> ```shell
> ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
> ```
>
> ==== 3. Configure OpenTofu
>
> ```shell
> cd setup
>
> # Copy the example config
> cp terraform.tfvars.example terraform.tfvars
>
> # Edit terraform.tfvars with your values:
> # - hcloud_token: Your Hetzner Cloud API token
> # - hcloud_image_id: The image ID from step 1
> # - flakehub_token: Your FlakeHub API token
> # - deploy_ssh_public_key: Contents of deploy_key.pub
> # - deploy_ssh_private_key: Contents of deploy_key (private key, for first-boot provisioning)
> # - (optional) ssh_public_key: Your personal SSH key for manual access
> ```
>
> ==== 4. Create the infrastructure
>
> ```shell
> # Initialize OpenTofu
> tofu init
>
> # Validate the configuration
> tofu validate
>
> # Create the resources
> tofu apply -auto-approve
>
> # Get the website URL
> export CRYPTPAD_URL=$(tofu output --json | jq -r .website.value)
>
> # Open the website (wait ~60 seconds for first deployment)
> open "${CRYPTPAD_URL}"
>
> # (optional) When you're done, destroy the resources
> tofu destroy -auto-approve
> ```
>
> === Automated deployment with GitHub Actions
>
> Once the server exists, GitHub Actions can automatically deploy NixOS
> configuration changes. Configure the following repository secrets:
>
> | | |
> | --- | --- |
> | Secret | Description |
> | `DEPLOY++_++SSH++_++PRIVATE++_++KEY` | The contents of the `deploy++_++key` file (private key) |
> | `HETZNER++_++SERVER++_++IP` | The server IP from `tofu output server++_++ip` |
>
> Using the `gh` CLI after completing link:#manual-deployment[manual
> deployment]:
>
> ```shell
> # Create a production environment (optional)
> gh api repos/{owner}/{repo}/environments/production --method PUT
>
> # Set secrets
> cat deploy_key | gh secret set DEPLOY_SSH_PRIVATE_KEY
> tofu output -raw server_ip | gh secret set HETZNER_SERVER_IP
> ```
>
> The workflow will automatically build, publish to FlakeHub, and deploy
> on pushes to main.
>
> == Documentation
>
> * [Docs](https://docs.outskirtslabs.com/nixos-hetzner-demo/next/)
> * https://docs.outskirtslabs.com/nixos-hetzner-demo/next/api[API
> Reference]
> * https://github.com/outskirtslabs/nixos-hetzner-demo/issues[Support via
> GitHub Issues]
> == How it works
>
> Here’s a high level walkthrough of what’s going on
>
> Initial setup (one-time)
>
> 1. Build NixOS base image locally using
> [nixos-hetzner](https://github.com/outskirtslabs/nixos-hetzner)
> 2. Upload image to Hetzner Cloud via hcloud-upload-image
> 3. Generate SSH deploy key for GitHub Actions
> 4. Configure terraform.tfvars with tokens, image ID, and keys
> First deployment (tofu apply)
>
> 1. OpenTofu creates Hetzner resources (SSH key, firewall, server)
> 2. Server boots with base NixOS image (includes determinate-nixd and fh)
> 3. Provisioner SSHs into server
> 4. Authenticates with FlakeHub using `determinate-nixd login token`
> 5. Runs `fh apply nixos ++<++flakeref++>++` to pull config from FlakeHub
> 6. Server downloads pre-built closure and switches to new configuration
> Subsequent deployments (via GitHub Actions)
>
> 1. Developer pushes changes to flake.nix
> 2. CI builds the NixOS closure
> 3. flakehub-push publishes closure to FlakeHub (with output paths cached)
> 4. Deploy job SSHs to server with the exact flakeref
> 5. Server runs `fh apply nixos` - downloads closure from FlakeHub Cache
> 6. Configuration applied in seconds (no building on server)
> === Nix flake
>
> The [`flake.nix`](./flake.nix) defines the NixOS configuration for
> the demo system:
>
> * Inputs:
> * `nixpkgs`: Custom nixpkgs fork with Hetzner Cloud tools (can be
> reverted to upstream nixpkgs once
> [PR #375551](https://github.com/NixOS/nixpkgs/pull/375551) is merged
> * [`nixos-hetzner`](https://github.com/outskirtslabs/nixos-hetzner):
> Hetzner Cloud image building tools
> * `determinate`: Determinate Nix distribution from FlakeHub
> * `fh`: FlakeHub CLI from FlakeHub
> * Outputs:
> * `nixosConfigurations.cryptpad-demo`: The CryptPad server
> configuration
> * `devShells.default`: Development environment with required tools
> === OpenTofu configuration
>
> The [`setup/`](./setup/) directory contains OpenTofu configuration
> for Hetzner Cloud:
>
> * `providers.tf`: Hetzner Cloud provider configuration
> * `variables.tf`: Input variables (API tokens, image ID, SSH keys, etc.)
> * `main.tf`: Server, firewall, and SSH key resources
> * `outputs.tf`: Server IP and website URL
> After server creation, Terraform provisions the server via SSH:
>
> 1. Authenticates with FlakeHub using `determinate-nixd login token`
> 2. Applies the NixOS configuration using `fh apply nixos`
> === GitHub Actions workflow
>
> The [`.github/workflows/ci.yml`](./.github/workflows/ci.yml)
> workflow:
>
> 1. Build job: Builds the NixOS closure and publishes to FlakeHub
> 2. Deploy job: SSHs to the server and runs `fh apply nixos` with the new
> closure
> === Continuous deployment
>
> Continuous deployments work by:
>
> 1. Pushing changes to the `flake.nix`
> 2. GitHub Actions builds the new closure
> 3. The closure is published to FlakeHub Cache
> 4. The deploy job SSHs to the server and applies the new configuration
> To demonstrate, make a change to the CryptPad configuration in
> `flake.nix` and push the changes.
>
> === Triggering rollbacks
>
> Use the `workflow++_++dispatch` event to manually trigger a deployment
> of a previous version.
>
> Why FlakeHub?
>
> Applying fully evaluated NixOS closures via
> [FlakeHub](https://flakehub.com) differs from typical deployments using
> Nix in several key ways:
>
> _Deployment speed_
>
> * FlakeHub deployment: The NixOS configuration is evaluated and built
> ahead of time. As the closure is pre-built and cached, the deployment
> process is faster. The server only needs to download and apply the
> pre-built closure.
> * Typical Nix deployment: The evaluation and build process happens
> during deployment, which can be time-consuming.
> _Resource utilization_
>
> * FlakeHub deployment: Offloads the computationally intensive tasks of
> evaluation and building to a controlled environment (e.g., a CI/CD
> pipeline), freeing up resources on the target server.
> * Typical Nix deployment: The target server must handle the evaluation
> and build process, which can be resource-intensive.
> _Scalability_
>
> * FlakeHub deployment: The pre-built and cached nature allows for rapid
> instance provisioning, making it ideal for auto-scaling scenarios.
> * Typical Nix deployment: The time required for evaluation and building
> on each new instance can introduce significant delays.
> In summary, applying a fully evaluated NixOS closure from
> [FlakeHub](https://flakehub.com) ensures that the exact same configuration
> is deployed every time, as the closure is a fixed, immutable artifact.
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
Git tags and Github release use the tag of the Determinate Nix version
used in the VM.
We use the `epoch` value to indicate breaking changes to the Hetzner
Cloud image NixOS configuration.
## ++[++UNRELEASED++]++
* no changes yet
## epoch-1 (2025-xx-xx)
This is an initial version of NixOS with Determinate Nix for Hetzner
Cloud.
## Contributing
# Contributing
Contributions are more than welcome!
For organization-wide expectations, see
[Outskirts Labs Contributing Guide](https://docs.outskirtslabs.com/contributing-guide).
A Nix devshell is available, which includes all development
dependencies, for contributing to nixos-hetzner.
Enter this shell with:
```sh
nix develop
```
If you use [`direnv`](https://direnv.net/), just run `direnv allow` and
you will be dropped in this devShell.
## Building
```bash
# Build a disk image to ./result/
nix build #diskImages.x86_64-linux.hetzner
# Set your HCLOUD_TOKEN
# ref: https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/
export HCLOUD_TOKEN=..
nix run #.apps.x86_64-linux.smoke-test -- --image-path result/nixos-image*img --architecture x86_64
nix run #.apps.x86_64-linux.smoke-test -- --image-path result/nixos-image*img --architecture x86_64 --keep-image-on-failure --debug
```
## Code quality
```bash
# Run the formatters
nix fmt
# Run the linters
nix flake check --print-build-logs
```
## Releasing
Releases are automated via Github Actions. Every detsys nix release
results in a new image.
Git tags and Github release use the tag of the Determinate Nix version
used in the VM.
However, we use the `epoch` value to indicate breaking changes to the
Hetzner Cloud image NixOS configuration.
## NixOS with Determinate Nix for Hetzner Cloud
# NixOS with Determinate Nix for Hetzner Cloud
>
> [(https://img.shields.io/badge/doc-outskirtslabs-orange.svg)]
> https://docs.outskirtslabs.com/open-source-vital-signs#static[image:https://img.shields.io/badge/status-static-blue.svg[status:
> static]]
>
> Hetzner is a price-competitive and conceptually simpler alternative to
> AWS and the other hyperscalers for the small orgs and teams that
> [I tend to work with](https://outskirtslabs.com).
>
> Using NixOS on Hetzner has traditionally been a bear, because Hetzner
> does not provide a NixOS image nor a straightforward way to create one.
> Most folks resort to using nixos-infect, nixos-anywhere to transmogrify
> a debian/ubuntu instance into NixOS.
>
> However several developments over the past year have changed the status
> quo:
>
> 1. [hcloud-upload-image](https://github.com/apricote/hcloud-upload-image)
> was released, a simple golang tool that takes a disk image as input and
> sideffects Hetzner Cloud in such a way that it creates a Snapshot from
> said image
> 2. [PR #375551](https://github.com/NixOS/nixpkgs/pull/375551) is making its
> way into nixpkgs which brings in `hcloud-upload-image` as well as the
> NixOS plumbing needed to produce hetzner images.
> 3. [FlakeHub Cache](https://flakehub.com/cache), available since late 2024,
> makes it _blazing_ fast to copy built closures into a running system.
> To be clear: I was not responsible for any of this. I’m taking advantage
> of the open-source efforts of others. This repo takes these disparate
> pieces and ties them together into an out-of-the-box solution for
> building Hetzner Cloud NixOS images.
>
> (and yes, even this repo is a derivative as I based it on
> [Determinate Systems](https://determinate.systems)'s
> [repo for AWS AMIs](https://github.com/DeterminateSystems/nixos-amis))
>
> '''''
>
> - 📌 NOTE
-
> This is a proof-of-concept repo maintained by me and not DetSys. I use
> something like this in prod, so what is here works, however don’t count
> on me to provide the same maintenance and upkeep like DetSys does for
> their official AWS AMIs.
>
>
> This repo makes available NixOS Hetzner Cloud images containing
> [Determinate Nix](https://docs.determinate.systems/determinate-nix)
>
> Images are available for these systems:
>
> * `x86++_++64-linux`
> * `aarch64-linux`
> On both systems, the images have these tools installed:
>
> * [Determinate Nix](https://docs.determinate.systems/determinate-nix),
> Determinate Systems' validated and secure
> [Nix](https://docs.determinate.systems/determinate-nix) distribution for
> enterprises. This includes
> https://docs.determinate.systems/determinate-nix#determinate-nixd[Determinate
> Nixd], a utility that enables you to log in to
> [FlakeHub](https://flakehub.com) from AWS using only this command (amongst
> other tasks):
>
> ```shell
> determinate-nixd login token --token-file
> ```
>
> Once logged in, your VM can access
> [FlakeHub Cache](https://docs.determinate.systems/flakehub/cache) and
> [private flakes](https://docs.determinate.systems/flakehub/private-flakes)
> for your organization.
> * [fh](https://docs.determinate.systems/flakehub/cli), the CLI for
> [FlakeHub](https://flakehub.com). You can use fh for things like
> [applying](https://docs.determinate.systems/flakehub/cli#apply-nixos)
> NixOS configurations uploaded to
> [FlakeHub Cache](https://docs.determinate.systems/flakehub/cache). Here’s
> an example:
>
> ```shell
> determinate-nixd login token --token-file
> fh apply nixos "my-org/my-flake/*#nixosConfigurations.my-nixos-configuration-output"
> ```
> Project status:
> **[Static](https://docs.outskirtslabs.com/open-source-vital-signs#static)**.
>
> == Example
>
> For a detailed example of deploying NixOS systems to Hetzner Cloud using
> these images, see our
> [nixos-hetzner-demo](https://github.com/outskirtslabs/nixos-hetzner-demo)
> repo.
>
> Here’s a simple way to get started:
>
> 1. https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/[Generate
> a Hetzner Cloud token]
> 2. Build and upload:
> ```bash
> HCLOUD_TOKEN=...your hcloud token...
> ARCH=x86_64-linux
> HCLOUD_ARCH="x86"
>
> # or
> # ARCH=aarch64-linux
> # HCLOUD_ARCH="arm"
> nix build "github:outskirtslabs/nixos-hetzner#diskImages.$ARCH.hetzner" --print-build-logs
>
> # inspect the image
> ls result/*
> IMAGE_PATH=$(ls result/*.img 2>/dev/null | head -1)
>
> # upload to hetzner cloud
> hcloud-upload-image upload \
> --image-path="$IMAGE_PATH" \
> --architecture="$HCLOUD_ARCH" \
> --description="nixos-hetzner image"
> ```
>
> == Documentation
>
> * [Docs](https://docs.outskirtslabs.com/nixos-hetzner/next/)
> * [API Reference](https://docs.outskirtslabs.com/nixos-hetzner/next/api)
> * https://github.com/outskirtslabs/nixos-hetzner/issues[Support via
> GitHub Issues]
> == Changelog
>
> -++>++ [CHANGELOG.adoc](./CHANGELOG.adoc)
>
> == License: Apache License 2.0
>
> Copyright © 2025 Casey Link casey@outskirtslabs.com
>
> Distributed under the
> [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html).
## ol.clave.acme.account
# ol.clave.acme.account
## validate-account
```clojure
(validate-account account)
```
Validate and normalize an account map, returning the normalized map or throwing ex-info.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L7-L10)
---
## get-primary-contact
```clojure
(get-primary-contact account)
```
Return the primary contact email (without scheme) or nil.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L12-L15)
---
## account-from-edn
```clojure
(account-from-edn registration-edn)
```
Parse an EDN string representing account registration metadata.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L17-L20)
---
## serialize
```clojure
(serialize account keypair)
```
Serialize an account map and keypair into a pretty-printed EDN artifact.
`keypair` is a `java.security.KeyPair`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L22-L27)
---
## deserialize
```clojure
(deserialize account-edn)
```
Deserialize an EDN artifact into [account keypair].
Returns a vector of [account keypair] where keypair is a `java.security.KeyPair`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L29-L34)
---
## generate-keypair
```clojure
(generate-keypair)
(generate-keypair opts)
```
Generate a new ACME account keypair.
Options map:
| key | description | default |
|--------|-----------------------------------------|---------|
| :algo | key algorithm (:p256, :p384, :ed25519) | :p256 |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L36-L45)
---
## create
```clojure
(create contact tos-agreed)
(create contact tos-agreed opts)
```
Construct an ACME account map suitable for directory interactions.
`contact` may be a single string or any sequential collection of strings; all
values must be `mailto:` URLs per RFC 8555 Section 7.3.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/account.clj#L47-L55)
## ol.clave.acme.challenge
# ol.clave.acme.challenge
Helpers for working with ACME challenges and authorizations.
## key-authorization
```clojure
(key-authorization challenge account-key)
```
Return the key authorization for `challenge` and `account-key`.
`challenge` may be a map with `::ol.clave.specs/token` or a raw token string.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L11-L19)
---
## dns01-key-authorization
```clojure
(dns01-key-authorization key-authorization)
(dns01-key-authorization challenge account-key)
```
Return the DNS-01 key authorization digest.
When called with a `challenge` map and `account-key`, computes the
key authorization first.
| arity | description |
|---------------------------|----------------------------------------------|
| `[key-authorization]` | digest the provided key authorization string |
| `[challenge account-key]` | compute key authorization then digest |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L21-L35)
---
## http01-resource-path
```clojure
(http01-resource-path challenge)
```
Return the HTTP-01 resource path for `challenge` or `token`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L37-L43)
---
## dns01-txt-name
```clojure
(dns01-txt-name domain-or-authorization)
```
Return the DNS-01 TXT record name for `domain` or `authorization`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L45-L51)
---
## wildcard?
```clojure
(wildcard? authorization)
```
Return true when the authorization declares a wildcard identifier.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L53-L56)
---
## identifier
```clojure
(identifier authorization)
```
Return the identifier value from an authorization map.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L58-L61)
---
## identifier-domain
```clojure
(identifier-domain authorization)
```
Return the identifier domain with any wildcard prefix removed.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L63-L69)
---
## token
```clojure
(token challenge)
```
Return the challenge token string.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L71-L74)
---
## find-by-type
```clojure
(find-by-type authorization type)
```
Return the first challenge in `authorization` matching `type`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L76-L79)
---
## acme-tls-1-protocol
ALPN protocol identifier for TLS-ALPN-01 challenges.
Use this value to detect ACME challenge handshakes in your TLS server’s
ALPN negotiation callback.
See RFC 8737 Section 6.2.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L81-L87)
---
## tlsalpn01-challenge-cert
```clojure
(tlsalpn01-challenge-cert identifier key-authorization)
(tlsalpn01-challenge-cert authorization challenge account-key)
```
Build a TLS-ALPN-01 challenge certificate.
This function has two arities:
Low-level arity `[identifier key-authorization]`:
- `identifier` - Map with `:type` ("dns" or "ip") and `:value`
- `key-authorization` - The computed key authorization string
Convenience arity `[authorization challenge account-key]`:
- `authorization` - Authorization map with `::acme/identifier`
- `challenge` - Challenge map with `::acme/token`
- `account-key` - Account keypair for computing key authorization
Returns a map with:
| key | description |
|--------------------|---------------------------------------------|
| `:certificate-der` | DER-encoded certificate bytes |
| `:certificate-pem` | PEM-encoded certificate string |
| `:private-key-der` | DER-encoded private key bytes (PKCS#8) |
| `:private-key-pem` | PEM-encoded private key string (PKCS#8) |
| `:x509` | Parsed `java.security.cert.X509Certificate` |
| `:keypair` | The generated `java.security.KeyPair` |
| `:identifier-type` | The identifier type from input |
| `:identifier-value`| The identifier value from input |
The certificate contains:
- Subject and Issuer: CN=ACME challenge
- SubjectAltName with the identifier (DNS name or IP address)
- Critical acmeValidationV1 extension (OID 1.3.6.1.5.5.7.1.31) containing
the SHA-256 digest of the key authorization
```clojure
;; Low-level usage with pre-computed key authorization
(tlsalpn01-challenge-cert {:type "dns" :value "example.com"}
"token.thumbprint")
;; Convenience usage with authorization and challenge maps
(tlsalpn01-challenge-cert authorization challenge account-key)
```
See RFC 8737 for TLS-ALPN-01 challenge specification.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/challenge.clj#L89-L138)
## ol.clave.acme.commands
# ol.clave.acme.commands
Plumbing layer ACME command API for interacting with an ACME server.
Every command takes an immutable ACME session map as its first argument and
returns a tuple where the first element is the updated session (with refreshed
nonces, account metadata, etc.). This keeps side effects explicit for callers.
Use this namespace when you need precise control over ACME interactions:
Session management:
- [`new-session`](#new-session), [`create-session`](#create-session), [`load-directory`](#load-directory), [`set-polling`](#set-polling)
Account operations:
- [`new-account`](#new-account), [`get-account`](#get-account), [`update-account-contact`](#update-account-contact)
- [`deactivate-account`](#deactivate-account), [`rollover-account-key`](#rollover-account-key)
- [`compute-eab-binding`](#compute-eab-binding)
Order lifecycle:
- [`new-order`](#new-order), [`get-order`](#get-order), [`poll-order`](#poll-order), [`finalize-order`](#finalize-order)
Authorization and challenges:
- [`get-authorization`](#get-authorization), [`poll-authorization`](#poll-authorization), [`deactivate-authorization`](#deactivate-authorization)
- [`respond-challenge`](#respond-challenge)
Certificate operations:
- [`get-certificate`](#get-certificate), [`revoke-certificate`](#revoke-certificate)
Renewal information (ARI per RFC 9773):
- [`get-renewal-info`](#get-renewal-info)
Terms of Service:
- [`check-terms-of-service`](#check-terms-of-service)
Pair these commands with `ol.clave.scope` to enforce timeouts and cancellation
across long-running workflows such as account setup or certificate issuance.
## new-session
```clojure
(new-session directory-url)
(new-session directory-url opts)
```
Create an ACME session value without issuing network requests.
Parameters:
- `directory-url` — ACME directory URL as a string.
- `opts` — optional map configuring the session. Recognised keys are
summarised below.
| Key | Type | Description |
|----------------|-------------------------|--------------------------------------------------------------------------|
| `:http-client` | map | Passed to `ol.clave.impl.http/http-client` to build the transport layer. |
| `:account-key` | `java.security.KeyPair` | Injects an existing key pair into the session. |
| `:account-kid` | string | Stores a known account URL for authenticated calls. |
| `:scope` | scope token | Overrides the default cancellation/timeout scope. |
Returns `[session nil]`, where `session` is a qualified map of
`::ol.clave.specs/*` keys suitable for subsequent commands.
Example:
```clojure
(require '[ol.clave.commands :as commands])
(let [[session _] (commands/new-session "https://acme.example/dir" {:http-client {}})]
(::ol.clave.specs/directory-url session))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L39-L67)
---
## load-directory
```clojure
(load-directory lease session)
(load-directory lease session opts)
```
Fetch the ACME directory document and attach it to `session`.
Uses a global cache with 12-hour TTL to avoid repeated fetches for
long-running servers managing multiple domains.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — session created by [`new-session`](#new-session) or [`create-session`](#create-session).
- `opts` — optional map with overrides.
Options:
| key | description |
|-----------|------------------------------------------- |
| `:force` | Bypass cache, fetch fresh from CA. |
| `:ttl-ms` | Custom cache TTL in milliseconds. |
Returns `[updated-session directory]`, where `directory` is the qualified map
described by `::ol.clave.specs/directory` and `updated-session` has the
directory attached.
When loaded from cache, the session will not have a nonce from this call;
the first JWS operation will fetch one via HEAD to newNonce.
Example:
```clojure
(require '[ol.clave.commands :as commands]
'[ol.clave.lease :as lease])
(let [bg (lease/background)
[session _] (commands/new-session "https://acme.example/dir" {:http-client {}})]
(commands/load-directory bg session))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L69-L106)
---
## set-polling
```clojure
(set-polling session opts)
```
Update default polling parameters in the session.
Parameters:
- `session` — ACME session map.
- `opts` — map with optional polling configuration keys.
Options:
| key | description |
|----------------|----------------------------------------------|
| `:interval-ms` | Default poll interval fallback (ms). |
| `:timeout-ms` | Default overall poll timeout (ms). |
Returns the updated session with new polling defaults.
Example:
```clojure
(-> session
(commands/set-polling {:interval-ms 2000 :timeout-ms 120000}))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L108-L130)
---
## create-session
```clojure
(create-session lease directory-url)
(create-session lease directory-url opts)
```
Build a session and eagerly download the ACME directory.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `directory-url` — ACME directory URL.
- `opts` — same options map accepted by [`new-session`](#new-session), optionally extended
with `:force` to bypass the directory cache, and `:ttl-ms` for custom cache TTL.
Returns `[session directory]` with the directory hydrated and incorporated
into `session`.
Example:
```clojure
(require '[ol.clave.commands :as commands]
'[ol.clave.lease :as lease])
(commands/create-session (lease/background) "https://acme.example/dir" {:http-client {}})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L132-L154)
---
## compute-eab-binding
```clojure
(compute-eab-binding eab-opts account-key endpoint)
```
Produce an External Account Binding structure for account creation.
Parameters:
- `eab-opts` — map with `:kid` and `:mac-key`, or `nil` to skip the binding.
- `account-key` — existing account key pair (`java.security.KeyPair`).
- `endpoint` — directory key identifying the `newAccount` URL.
Returns the binding map described by RFC 8555 §7.3.4 or `nil` when
`eab-opts` is missing. Invalid base64 encoding raises
`::ol.clave.errors/invalid-eab`.
Example:
```clojure
(require '[ol.clave.commands :as commands])
(commands/compute-eab-binding {:kid "kid-123" :mac-key "q83l..."}
account-key
"https://acme.example/new-account")
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L156-L177)
---
## new-account
```clojure
(new-account lease session account)
(new-account lease session account opts)
```
Register a new ACME account at the directory’s `newAccount` endpoint.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated or unauthenticated ACME session map.
- `account` — account data using `::ol.clave.specs/*` keys.
- `opts` — optional map supporting the keys listed below.
| Key | Description |
| --- | --- |
| `:external-account` | `{:kid string :mac-key bytes-or-base64}` enabling External Account Binding. |
Returns `[updated-session normalized-account]`. The session gains the account
KID and the response account is merged onto the supplied `account` map.
Example:
```clojure
(require '[ol.clave.commands :as commands]
'[ol.clave.account :as account]
'[ol.clave.lease :as lease])
(let [[acct key] (account/deserialize (slurp "test/fixtures/test-account.edn"))]
(commands/new-account (lease/background) session acct))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L179-L207)
---
## find-account-by-key
```clojure
(find-account-by-key lease session)
(find-account-by-key lease session opts)
```
Look up an existing ACME account by its public key.
Uses the newAccount endpoint with `onlyReturnExisting: true` to find an
account without creating one. This is useful for key recovery scenarios
where you have the account key but lost the account URL.
Parameters:
- `lease` - A lease for cooperative cancellation.
- `session` - Session with account key set (via `:account-key` option).
Returns `[updated-session account-kid]` where `account-kid` is the account
URL string. The session is updated with the account KID.
Throws `::ol.clave.errors/account-not-found` if no account exists for the key.
Throws `::ol.clave.errors/invalid-account-key` if session has no account key.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L209-L228)
---
## get-account
```clojure
(get-account lease session account)
(get-account lease session account opts)
```
Retrieve the current state of an ACME account via POST-as-GET.
Parameters:
- `lease` - A lease for cooperative cancellation.
- `session` — session containing `::ol.clave.specs/account-kid` and
`::ol.clave.specs/account-key`.
- `account` — baseline account map that will be merged with the server
response.
Returns `[updated-session account-map]` where `account-map` is the merged
account including the authoritative contact info and account status.
Example:
```clojure
(require '[ol.clave.commands :as commands])
(commands/get-account lease session account)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L230-L252)
---
## update-account-contact
```clojure
(update-account-contact lease session account contacts)
(update-account-contact lease session account contacts opts)
```
Replace the contact URIs registered for an ACME account.
Parameters:
- `lease` - A lease for cooperative cancellation.
- `session` — authenticated session.
- `account` — current account map.
- `contacts` — vector of `mailto:` URIs to set on the account.
Returns `[updated-session updated-account]` with contacts normalised to a
vector of strings sourced from the server response.
Example:
```clojure
(require '[ol.clave.commands :as commands])
(commands/update-account-contact lease session account ["mailto:admin@example.com"])
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L254-L275)
---
## deactivate-account
```clojure
(deactivate-account lease session account)
(deactivate-account lease session account opts)
```
Deactivate an ACME account by issuing a status change request.
Parameters:
- `lease` - A lease for cooperative cancellation.
- `session` — authenticated session.
- `account` — account map with identifying information.
Returns `[updated-session deactivated-account]`. Subsequent account
operations will fail with `::ol.clave.errors/unauthorized-account`.
Example:
```clojure
(require '[ol.clave.commands :as commands])
(commands/deactivate-account lease session account)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L277-L297)
---
## rollover-account-key
```clojure
(rollover-account-key lease session account new-account-key)
(rollover-account-key lease session account new-account-key opts)
```
Replace the account key pair using the directory `keyChange` endpoint.
Parameters:
- `lease` - A lease for cooperative cancellation.
- `session` — authenticated session containing the current account key and KID.
- `account` — account data used to verify the new key.
- `new-account-key` — `java.security.KeyPair` to install.
Returns `[updated-session verified-account]` with the session updated to store
`new-account-key`. Verification failures raise
`::ol.clave.errors/account-key-rollover-verification-failed`.
Example:
```clojure
(require '[ol.clave.commands :as commands]
'[ol.clave.account :as account])
(let [new-key (account/generate-keypair)]
(commands/rollover-account-key lease session account new-key))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L299-L323)
---
## new-order
```clojure
(new-order lease session order)
(new-order lease session order opts)
```
Create a new ACME order for the supplied identifiers.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session with account key and KID.
- `order` — map containing `::ol.clave.specs/identifiers` and optional
`::ol.clave.specs/notBefore` / `::ol.clave.specs/notAfter`.
- `opts` — optional map with overrides.
Options:
| key | description |
|------------|-------------|
| `:profile` | Optional profile name as a string when the directory advertises `:profiles`. |
Returns `[updated-session order]` where `order` is the normalized order map
including `::ol.clave.specs/order-location`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L325-L346)
---
## get-order
```clojure
(get-order lease session order-url)
(get-order lease session order-url opts)
```
Retrieve the current state of an order via POST-as-GET.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `order-url` — order URL string, or an order map that includes
`::ol.clave.specs/order-location`.
- `opts` — optional map with overrides.
Returns `[updated-session order]` with the latest order data.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L348-L362)
---
## poll-order
```clojure
(poll-order lease session order-url)
```
Poll an order URL until it reaches a terminal status.
Timeout is the lesser of session’s poll-timeout and the lease’s deadline.
Configure polling defaults via [`set-polling`](#set-polling).
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `order-url` — order URL string.
Returns `[updated-session order]` on success or throws on invalid/timeout.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L364-L377)
---
## finalize-order
```clojure
(finalize-order lease session order csr)
(finalize-order lease session order csr opts)
```
Finalize an order by submitting a CSR.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `order` — normalized order map with `::ol.clave.specs/status` and
`::ol.clave.specs/finalize`.
- `csr` — map containing `:csr-b64url` from [`ol.clave.certificate.impl.csr/create-csr`](api/ol-clave-certificate-impl-csr.adoc#create-csr).
- `opts` — optional map with overrides.
Returns `[updated-session order]` with the updated order state.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L379-L394)
---
## get-certificate
```clojure
(get-certificate lease session certificate-url)
(get-certificate lease session certificate-url opts)
```
Download a PEM certificate chain from the certificate URL via POST-as-GET.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session carrying HTTP configuration.
- `certificate-url` — certificate URL from an order.
- `opts` — optional map with overrides.
Returns `[updated-session result]` where `result` includes `:chains` and
`:preferred` entries with parsed PEM data.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L396-L410)
---
## get-authorization
```clojure
(get-authorization lease session authorization-url)
(get-authorization lease session authorization-url opts)
```
Fetch an authorization resource via POST-as-GET.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `authorization-url` — authorization URL string, or an authorization map
containing `::ol.clave.specs/authorization-location`.
- `opts` — optional map with overrides.
Returns `[updated-session authorization]`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L412-L426)
---
## poll-authorization
```clojure
(poll-authorization lease session authorization-url)
```
Poll an authorization URL until it reaches a terminal status.
Timeout is the lesser of session' poll-timeout and lease’s deadline.
Configure polling defaults via [`set-polling`](#set-polling).
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `authorization-url` — authorization URL string.
Returns `[updated-session authorization]` on success or throws when invalid,
unusable, or timed out.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L428-L442)
---
## deactivate-authorization
```clojure
(deactivate-authorization lease session authorization-url)
(deactivate-authorization lease session authorization-url opts)
```
Deactivate an authorization by sending a status update.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `authorization-url` — authorization URL string, or authorization map.
- `opts` — optional map with overrides.
Returns `[updated-session authorization]`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L444-L457)
---
## new-authorization
```clojure
(new-authorization lease session identifier)
(new-authorization lease session identifier opts)
```
Create a pre-authorization for an identifier via the newAuthz endpoint.
Pre-authorization (RFC 8555 Section 7.4.1) allows clients to obtain
authorization proactively, outside the context of a specific order.
This is useful for hosting providers who want to authorize domains
before virtual servers are created.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `identifier` — map with `:type` and `:value` keys.
- `opts` — optional map with overrides.
Pre-authorization cannot be used with wildcard identifiers.
Not all ACME servers support this endpoint.
Returns `[updated-session authorization]`.
Throws:
- `::ol.clave.errors/pre-authorization-unsupported` if server does not
advertise newAuthz endpoint.
- `::ol.clave.errors/wildcard-identifier-not-allowed` if identifier is
a wildcard.
- `::ol.clave.errors/pre-authorization-failed` if server rejects the request.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L459-L487)
---
## respond-challenge
```clojure
(respond-challenge lease session challenge)
(respond-challenge lease session challenge opts)
```
Notify the ACME server that a challenge response is ready.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session.
- `challenge` — challenge map containing `::ol.clave.specs/url`.
- `opts` — optional map with overrides.
Options:
| key | description |
|------------|--------------------------------------|
| `:payload` | Override the default `{}` payload. |
Returns `[updated-session challenge]`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L489-L508)
---
## revoke-certificate
```clojure
(revoke-certificate lease session certificate)
(revoke-certificate lease session certificate opts)
```
Revoke a certificate via the directory’s revokeCert endpoint.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` — authenticated session or session with directory loaded.
- `certificate` — `java.security.cert.X509Certificate` or DER bytes.
- `opts` — optional map with overrides.
Options:
| key | description |
|----------------|----------------------------------------------------------|
| `:reason` | RFC 5280 reason code integer (0-6, 8-10). |
| `:signing-key` | `java.security.KeyPair` for certificate-key authorization. |
When `:signing-key` is provided, uses certificate-key authorization with
JWK-embedded JWS. Otherwise uses account-key authorization requiring an
authenticated session.
Returns `[updated-session nil]` on success.
Example:
```clojure
(commands/revoke-certificate lease session cert {:reason 1})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L510-L539)
---
## get-renewal-info
```clojure
(get-renewal-info lease session cert-or-id)
(get-renewal-info lease session cert-or-id opts)
```
Fetch ACME Renewal Information (ARI) for a certificate per RFC 9773.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` - ACME session with directory loaded.
- `cert-or-id` - X509Certificate or precomputed renewal identifier string.
- `opts` - optional map with overrides.
When `cert-or-id` is a certificate, the renewal identifier is derived from
the Authority Key Identifier extension and serial number.
Returns `[updated-session renewal-info]` where `renewal-info` contains
`:suggested-window` (map with `:start` and `:end` instants),
`:retry-after-ms`, and optional `:explanation-url`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L541-L559)
---
## check-terms-of-service
```clojure
(check-terms-of-service lease session)
(check-terms-of-service lease session opts)
```
Check for Terms of Service changes by comparing directory meta values.
Parameters:
- `lease` — lease for cancellation/timeout control.
- `session` - ACME session with directory already loaded.
- `opts` - optional map with overrides.
Refreshes the directory from the server and compares the `termsOfService`
field in the meta section with the previously loaded value.
Returns `[updated-session tos-change]` where `tos-change` contains:
- `:changed?` - true if termsOfService URL changed
- `:previous` - previous termsOfService URL or nil
- `:current` - current termsOfService URL or nil
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/commands.clj#L561-L579)
## ol.clave.acme.impl.account
# ol.clave.acme.impl.account
## validate-account
```clojure
(validate-account account)
```
See [`ol.clave.acme.account/validate-account`](api/ol-clave-acme-account.adoc#validate-account)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L43-L64)
---
## get-primary-contact
```clojure
(get-primary-contact account)
```
See [`ol.clave.acme.account/get-primary-contact`](api/ol-clave-acme-account.adoc#get-primary-contact)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L66-L71)
---
## account-from-edn
```clojure
(account-from-edn registration-edn)
```
See [`ol.clave.acme.account/account-from-edn`](api/ol-clave-acme-account.adoc#account-from-edn)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L73-L84)
---
## serialize
```clojure
(serialize account keypair)
```
See [`ol.clave.acme.account/serialize`](api/ol-clave-acme-account.adoc#serialize)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L93-L105)
---
## deserialize
```clojure
(deserialize account-edn)
```
See [`ol.clave.acme.account/deserialize`](api/ol-clave-acme-account.adoc#deserialize)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L107-L124)
---
## generate-keypair
```clojure
(generate-keypair)
(generate-keypair {:keys [algo] :or {algo :p256}})
```
See [`ol.clave.acme.account/generate-keypair`](api/ol-clave-acme-account.adoc#generate-keypair)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L126-L131)
---
## create
```clojure
(create contact tos-agreed)
(create contact tos-agreed _)
```
See [`ol.clave.acme.account/create`](api/ol-clave-acme-account.adoc#create)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/account.clj#L133-L157)
## ol.clave.acme.impl.ari
# ol.clave.acme.impl.ari
ARI identifier derivation helpers per RFC 9773.
Extracts the Authority Key Identifier keyIdentifier and serial number
from an X509Certificate and builds the unique renewal identifier string.
## authority-key-identifier
```clojure
(authority-key-identifier cert)
```
Extract the keyIdentifier bytes from the AKI extension of a certificate.
Parameters:
- `cert` - X509Certificate to extract AKI from.
Returns the keyIdentifier bytes or throws `::errors/renewal-info-invalid`
if the AKI extension is missing or does not contain a keyIdentifier.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L23-L57)
---
## serial-der-bytes
```clojure
(serial-der-bytes cert)
```
Return the DER-encoded serial number bytes of a certificate.
Per RFC 9773, this is the two’s complement encoding of the serial number
with a leading zero byte if the high bit is set (to preserve positive sign).
Parameters:
- `cert` - X509Certificate to extract serial from.
Returns the DER-encoded serial number bytes (without tag and length).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L59-L73)
---
## renewal-id
```clojure
(renewal-id cert)
```
Derive the ARI renewal identifier from a certificate.
The identifier is: base64url(AKI keyIdentifier) || '.' || base64url(serial DER)
with all trailing padding ('=') stripped per RFC 9773.
Parameters:
- `cert` - X509Certificate to derive identifier from.
Returns the renewal identifier string.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L75-L90)
---
## normalize-renewal-info
```clojure
(normalize-renewal-info body retry-after-ms)
```
Normalize a RenewalInfo response from the server.
Parameters:
- `body` - parsed JSON response body as a map (keyword or string keys).
- `retry-after-ms` - Retry-After value in milliseconds.
Returns a normalized map with `:suggested-window`, optional `:explanation-url`,
and `:retry-after-ms`. Throws `::errors/renewal-info-invalid` if the response
is malformed or the window is invalid.
The suggested window must have end strictly after start per RFC 9773.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/ari.clj#L107-L144)
## ol.clave.acme.impl.authorization
# ol.clave.acme.impl.authorization
## normalize-authorization
```clojure
(normalize-authorization authorization account-key location)
```
Normalize an authorization response into qualified keys and computed data.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L12-L27)
---
## authorization-valid?
```clojure
(authorization-valid? authorization)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L29-L31)
---
## authorization-invalid?
```clojure
(authorization-invalid? authorization)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L33-L35)
---
## authorization-unusable?
```clojure
(authorization-unusable? authorization)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L37-L39)
---
## authorization-problem
```clojure
(authorization-problem authorization)
```
Return the most relevant problem map from an authorization.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L41-L45)
---
## wildcard-identifier?
```clojure
(wildcard-identifier? identifier)
```
Return true when `identifier` is a wildcard (value starts with *.).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/authorization.clj#L47-L52)
## ol.clave.acme.impl.challenge
# ol.clave.acme.impl.challenge
## key-authorization
```clojure
(key-authorization token account-keypair)
```
See [`ol.clave.acme.challenge/key-authorization`](api/ol-clave-acme-challenge.adoc#key-authorization)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L14-L19)
---
## dns01-key-authorization
```clojure
(dns01-key-authorization key-authorization)
```
See [`ol.clave.acme.challenge/dns01-key-authorization`](api/ol-clave-acme-challenge.adoc#dns01-key-authorization)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L21-L27)
---
## http01-resource-path
```clojure
(http01-resource-path token)
```
See [`ol.clave.acme.challenge/http01-resource-path`](api/ol-clave-acme-challenge.adoc#http01-resource-path)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L29-L32)
---
## dns01-txt-name
```clojure
(dns01-txt-name domain)
```
See [`ol.clave.acme.challenge/dns01-txt-name`](api/ol-clave-acme-challenge.adoc#dns01-txt-name)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L34-L38)
---
## normalize-challenge
```clojure
(normalize-challenge challenge)
(normalize-challenge challenge account-key)
```
Qualify a challenge map and attach computed key authorization when possible.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/challenge.clj#L40-L50)
## ol.clave.acme.impl.commands
# ol.clave.acme.impl.commands
## default-poll-interval-ms
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L28-L28)
---
## default-poll-timeout-ms
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L29-L29)
---
## new-session
```clojure
(new-session directory-url {:keys [http-client account-key account-kid]})
```
See [`ol.clave.acme.commands/new-session`](api/ol-clave-acme-commands.adoc#new-session)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L31-L43)
---
## set-polling
```clojure
(set-polling session {:keys [interval-ms timeout-ms]})
```
See [`ol.clave.acme.commands/set-polling`](api/ol-clave-acme-commands.adoc#set-polling)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L45-L50)
---
## load-directory
```clojure
(load-directory lease session)
(load-directory lease {::acme/keys [directory-url] :as session} opts)
```
See [`ol.clave.acme.commands/load-directory`](api/ol-clave-acme-commands.adoc#load-directory)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L52-L80)
---
## create-session
```clojure
(create-session lease directory-url opts)
```
See [`ol.clave.acme.commands/create-session`](api/ol-clave-acme-commands.adoc#create-session)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L82-L87)
---
## compute-eab-binding
```clojure
(compute-eab-binding eab-opts account-key endpoint)
```
See [`ol.clave.acme.commands/compute-eab-binding`](api/ol-clave-acme-commands.adoc#compute-eab-binding)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L89-L104)
---
## new-account
```clojure
(new-account lease session account)
(new-account lease session account opts)
```
See [`ol.clave.acme.commands/new-account`](api/ol-clave-acme-commands.adoc#new-account)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L106-L145)
---
## find-account-by-key
```clojure
(find-account-by-key lease session)
(find-account-by-key lease session _opts)
```
See [`ol.clave.acme.commands/find-account-by-key`](api/ol-clave-acme-commands.adoc#find-account-by-key)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L147-L181)
---
## get-account
```clojure
(get-account lease session account)
(get-account lease session account _opts)
```
See [`ol.clave.acme.commands/get-account`](api/ol-clave-acme-commands.adoc#get-account)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L242-L252)
---
## update-account-contact
```clojure
(update-account-contact lease session account contacts)
(update-account-contact lease session account contacts _opts)
```
See [`ol.clave.acme.commands/update-account-contact`](api/ol-clave-acme-commands.adoc#update-account-contact)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L254-L270)
---
## deactivate-account
```clojure
(deactivate-account lease session account)
(deactivate-account lease session account _opts)
```
See [`ol.clave.acme.commands/deactivate-account`](api/ol-clave-acme-commands.adoc#deactivate-account)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L272-L281)
---
## rollover-account-key
```clojure
(rollover-account-key lease session account new-account-key)
(rollover-account-key lease session account new-account-key _opts)
```
See [`ol.clave.acme.commands/rollover-account-key`](api/ol-clave-acme-commands.adoc#rollover-account-key)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L283-L316)
---
## new-order
```clojure
(new-order lease session order)
(new-order lease session order opts)
```
See [`ol.clave.acme.commands/new-order`](api/ol-clave-acme-commands.adoc#new-order)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L318-L351)
---
## get-order
```clojure
(get-order lease session order-or-url)
(get-order lease session order-or-url _opts)
```
See [`ol.clave.acme.commands/get-order`](api/ol-clave-acme-commands.adoc#get-order)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L371-L383)
---
## poll-order
```clojure
(poll-order lease session order-url)
```
See [`ol.clave.acme.commands/poll-order`](api/ol-clave-acme-commands.adoc#poll-order)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L385-L432)
---
## finalize-order
```clojure
(finalize-order lease session order csr)
(finalize-order lease session order csr _opts)
```
See [`ol.clave.acme.commands/finalize-order`](api/ol-clave-acme-commands.adoc#finalize-order)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L434-L479)
---
## get-authorization
```clojure
(get-authorization lease session authorization-or-url)
(get-authorization lease session authorization-or-url _opts)
```
See [`ol.clave.acme.commands/get-authorization`](api/ol-clave-acme-commands.adoc#get-authorization)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L499-L509)
---
## poll-authorization
```clojure
(poll-authorization lease session authorization-url)
```
See [`ol.clave.acme.commands/poll-authorization`](api/ol-clave-acme-commands.adoc#poll-authorization)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L511-L566)
---
## deactivate-authorization
```clojure
(deactivate-authorization lease session authorization-or-url)
(deactivate-authorization lease session authorization-or-url _opts)
```
See [`ol.clave.acme.commands/deactivate-authorization`](api/ol-clave-acme-commands.adoc#deactivate-authorization)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L568-L591)
---
## new-authorization
```clojure
(new-authorization lease session identifier)
(new-authorization lease session identifier _opts)
```
See [`ol.clave.acme.commands/new-authorization`](api/ol-clave-acme-commands.adoc#new-authorization)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L593-L636)
---
## respond-challenge
```clojure
(respond-challenge lease session challenge)
(respond-challenge lease session challenge opts)
```
See [`ol.clave.acme.commands/respond-challenge`](api/ol-clave-acme-commands.adoc#respond-challenge)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L638-L659)
---
## get-certificate
```clojure
(get-certificate lease session certificate-url)
(get-certificate lease session certificate-url _opts)
```
See [`ol.clave.acme.commands/get-certificate`](api/ol-clave-acme-commands.adoc#get-certificate)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L661-L689)
---
## revoke-certificate
```clojure
(revoke-certificate lease session certificate)
(revoke-certificate lease session certificate opts)
```
See [`ol.clave.acme.commands/revoke-certificate`](api/ol-clave-acme-commands.adoc#revoke-certificate)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L691-L738)
---
## get-renewal-info
```clojure
(get-renewal-info lease session cert-or-id)
(get-renewal-info lease session cert-or-id _opts)
```
See [`ol.clave.acme.commands/get-renewal-info`](api/ol-clave-acme-commands.adoc#get-renewal-info)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L755-L792)
---
## check-terms-of-service
```clojure
(check-terms-of-service lease session)
(check-terms-of-service lease session _opts)
```
See [`ol.clave.acme.commands/check-terms-of-service`](api/ol-clave-acme-commands.adoc#check-terms-of-service)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/commands.clj#L794-L803)
## ol.clave.acme.impl.directory-cache
# ol.clave.acme.impl.directory-cache
Global directory cache with TTL for ACME directory responses.
ACME directories rarely change, so caching them avoids unnecessary network
requests for long-running servers managing many domains.
The cache is keyed by directory URL with a 12-hour default TTL.
Stale entries remain until replaced (no background cleanup).
## default-ttl-ms
Default TTL of 12 hours in milliseconds.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L10-L12)
---
## cache-get
```clojure
(cache-get url)
(cache-get url ttl-ms)
```
Returns cached directory for url if present and fresh, else nil.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L25-L31)
---
## cache-put!
```clojure
(cache-put! url directory)
```
Stores directory in cache with current timestamp. Returns directory.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L33-L38)
---
## cache-clear!
```clojure
(cache-clear!)
```
Clears entire directory cache. Useful for testing.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L40-L43)
---
## cache-evict!
```clojure
(cache-evict! url)
```
Removes single entry from cache.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/directory_cache.clj#L45-L48)
## ol.clave.acme.impl.http.interceptors
# ol.clave.acme.impl.http.interceptors
## ->uri
```clojure
(->uri uri)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L21-L31)
---
## accept-header
Request: adds `:accept` header. Only supported value is `:json`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L55-L67)
---
## uri-with-query
```clojure
(uri-with-query uri new-query)
```
We can’t use the URI constructor because it encodes all arguments for us.
See https://stackoverflow.com/a/77971448/6264
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L85-L99)
---
## query-params
Request: encodes `:query-params` map and appends to `:uri`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L101-L110)
---
## form-params
Request: encodes `:form-params` map and adds `:body`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L123-L133)
---
## decompress-body
Response: decompresses body based on "content-encoding" header. Valid values: `gzip` and `deflate`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L177-L184)
---
## decode-body
Response: based on the value of `:as` in request, decodes as `:string`, `:stream` or `:bytes`. Defaults to `:string`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L191-L203)
---
## construct-uri
Request: construct uri from map
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L205-L212)
---
## request-method
Request: normalize :method option
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L214-L221)
---
## unexceptional-statuses
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L223-L224)
---
## throw-on-exceptional-status-code
Response: throw on exceptional status codes
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L226-L235)
---
## parse-json-body
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L237-L251)
---
## default-interceptors
Default interceptor chain. Interceptors are called in order for request and in reverse order for response.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/http/interceptors.clj#L253-L263)
## ol.clave.acme.impl.order
# ol.clave.acme.impl.order
## create-identifier
```clojure
(create-identifier identifier)
(create-identifier identifier-type identifier-value)
```
See [`ol.clave.acme.order/create-identifier`](api/ol-clave-acme-order.adoc#create-identifier)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L21-L37)
---
## create
```clojure
(create identifiers)
(create identifiers opts)
```
See [`ol.clave.acme.order/create`](api/ol-clave-acme-order.adoc#create)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L39-L62)
---
## build-order-payload
```clojure
(build-order-payload order)
```
Build an ACME newOrder payload from a qualified order map.
Parameters:
- `order` - Order map with `::acme/identifiers` and optional
`::acme/notBefore`, `::acme/notAfter`, `::acme/replaces`.
The `replaces` field (RFC 9773) links a renewal order to its predecessor
certificate using the ARI unique identifier format.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L74-L101)
---
## normalize-order
```clojure
(normalize-order order location)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L103-L113)
---
## ensure-identifiers-consistent
```clojure
(ensure-identifiers-consistent expected order)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L115-L122)
---
## order-ready?
```clojure
(order-ready? order)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L124-L126)
---
## order-terminal?
```clojure
(order-terminal? order)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/order.clj#L128-L130)
## ol.clave.acme.impl.revocation
# ol.clave.acme.impl.revocation
Pure helpers for certificate revocation payload construction and validation.
This namespace handles:
- Extracting DER bytes from X509Certificate or raw bytes
- Constructing revocation payloads with base64url-encoded certificates
- Validating RFC 5280 reason codes for ACME revocation
## valid-reason?
```clojure
(valid-reason? reason)
```
Return true if `reason` is a valid RFC 5280 revocation reason code for ACME.
Valid codes are 0-6 and 8-10. Code 7 is unused in RFC 5280.
Returns false for non-integer values.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/revocation.clj#L21-L28)
---
## certificate->der
```clojure
(certificate->der certificate)
```
Extract DER-encoded bytes from a certificate.
Accepts either:
- `java.security.cert.X509Certificate` - extracts via `.getEncoded()`
- `byte[]` - returns as-is
Returns the DER-encoded certificate bytes.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/revocation.clj#L30-L49)
---
## payload
```clojure
(payload certificate)
(payload certificate opts)
```
Construct a revocation request payload.
Parameters:
- `certificate` - `X509Certificate` or DER bytes
- `opts` - optional map with `:reason` (RFC 5280 reason code)
Returns a map with:
- `:certificate` - base64url-encoded DER
- `:reason` - reason code (when provided in opts)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/revocation.clj#L51-L67)
## ol.clave.acme.impl.tos
# ol.clave.acme.impl.tos
Terms of Service change detection helpers.
Compares prior and current directory meta values to detect ToS changes.
## compare-terms
```clojure
(compare-terms previous current)
```
Compare previous and current directory meta maps for ToS changes.
Parameters:
- `previous` - previous directory meta map with `::specs/termsOfService`.
- `current` - current directory meta map with `::specs/termsOfService`.
Returns a map with:
- `:changed?` - true if termsOfService values differ
- `:previous` - previous termsOfService URL or nil
- `:current` - current termsOfService URL or nil
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/impl/tos.clj#L10-L26)
## ol.clave.acme.order
# ol.clave.acme.order
Helpers for building and inspecting ACME orders.
## create-identifier
```clojure
(create-identifier identifier)
(create-identifier identifier-type identifier-value)
```
Construct an identifier map from `identifier` or `type` and `value`.
`type` may be a string or keyword such as `dns` or `:dns`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L9-L16)
---
## create
```clojure
(create identifiers)
(create identifiers opts)
```
Construct an order map with the supplied identifiers and options.
Options:
| key | description |
|---------------|-----------------------------------------------|
| `:not-before` | Optional notBefore instant or RFC3339 string. |
| `:not-after` | Optional notAfter instant or RFC3339 string. |
| `:profile` | Optional profile name as a string. |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L18-L32)
---
## identifiers
```clojure
(identifiers order)
```
Return the identifiers on an order map.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L34-L37)
---
## authorizations
```clojure
(authorizations order)
```
Return the authorization URLs from an order map.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L39-L42)
---
## url
```clojure
(url order)
```
Return the order URL from an order map.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L44-L47)
---
## certificate-url
```clojure
(certificate-url order)
```
Return the certificate URL from an order map.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/order.clj#L49-L52)
## ol.clave.acme.solver.http
# ol.clave.acme.solver.http
HTTP-01 challenge solver with Ring middleware.
This namespace provides a complete HTTP-01 challenge solution for clojure ring servers.
* [`solver`](#solver) creates a solver map for use with `ol.clave/obtain-certificate`
* [`wrap-acme-challenge`](#wrap-acme-challenge) is Ring middleware that serves challenge responses
Usage:
```clojure
(require '[ol.clave.acme.solver.http :as http-solver])
(def my-solver (http-solver/solver))
;; Add middleware to your Ring app
(def app (-> your-handler
(http-solver/wrap-acme-challenge my-solver)))
;; Use solver with obtain-certificate
(clave/obtain-certificate lease session identifiers cert-key
{:http-01 my-solver} {})
```
## solver
```clojure
(solver)
```
Create an HTTP-01 solver that registers challenges in a registry.
Creates its own registry atom internally.
The [`wrap-acme-challenge`](#wrap-acme-challenge) middleware reads from this registry to serve
challenge responses.
Returns a solver map with `:present`, `:cleanup`, and `:registry`.
Example:
```clojure
(def my-solver (solver))
;; Pass solver to middleware
(def app (-> handler (wrap-acme-challenge my-solver)))
;; Use with obtain-certificate
(clave/obtain-certificate lease session identifiers cert-key
{:http-01 my-solver} {})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/http.clj#L32-L62)
---
## wrap-acme-challenge
```clojure
(wrap-acme-challenge handler solver)
```
Ring middleware that serves ACME HTTP-01 challenge responses.
Intercepts requests to `/.well-known/acme-challenge/{token}` and returns
the key-authorization from the solver’s registry.
Other requests pass through to the wrapped handler.
Parameters:
| name | description |
|-----------|------------------------------|
| `handler` | The Ring handler to wrap |
| `solver` | Solver created by [`solver`](#solver) |
Response behavior:
- Returns 200 with key-authorization as plain text if token found
- Returns 404 if token not in registry
- Passes through to handler for non-challenge paths
Example:
```clojure
(def my-solver (solver))
(def app
(-> my-handler
(wrap-acme-challenge my-solver)))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/http.clj#L64-L105)
---
## handler
```clojure
(handler solver)
```
Standalone Ring handler for ACME HTTP-01 challenges.
Use this when you want a dedicated server for challenges rather than
integrating with an existing application.
Parameters:
| name | description |
|----------|------------------------------|
| `solver` | Solver created by [`solver`](#solver) |
Returns a Ring handler function.
Example:
```clojure
(def my-solver (solver))
;; Start a dedicated challenge server
(run-jetty (http-solver/handler my-solver) {:port 80})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/http.clj#L107-L129)
## ol.clave.acme.solver.tls-alpn
# ol.clave.acme.solver.tls-alpn
TLS-ALPN-01 challenge solver.
Provides two solver implementations:
- [`bootstrap-solver`](#bootstrap-solver) - Starts temporary SSLServerSocket (before TLS server running)
- [`integrated-solver`](#integrated-solver) - Registers with KeyManager (when TLS server running)
For typical use, [`switchable-solver`](#switchable-solver) creates a solver that starts in bootstrap
mode and can be switched to integrated mode after your TLS server starts.
Usage:
```clojure
(require '[ol.clave.acme.solver.tls-alpn :as tls-alpn])
(def solver (tls-alpn/switchable-solver {:port 443}))
;; Use solver with automation system
(auto/start {:solvers {:tls-alpn-01 solver} ...})
(auto/manage-domains system ["example.com"])
;; Pass registry to sni-alpn-ssl-context
(jetty-ext/sni-alpn-ssl-context lookup-fn (tls-alpn/challenge-registry solver))
;; After TLS server starts, switch to integrated mode for renewals
(tls-alpn/switch-to-integrated! solver)
```
## bootstrap-solver
```clojure
(bootstrap-solver {:keys [port] :or {port 443}})
```
TLS-ALPN-01 solver that starts a temporary server.
Use for initial certificate acquisition before the main TLS server starts.
Starts an SSLServerSocket during `:present`, stops it during `:cleanup`.
Options:
| key | description | default |
|---------|---------------------------|---------|
| `:port` | Port for challenge server | 443 |
Returns a solver map with `:present` and `:cleanup` functions.
```clojure
(def solver (bootstrap-solver {:port 8443}))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L120-L157)
---
## integrated-solver
```clojure
(integrated-solver)
```
TLS-ALPN-01 solver that registers with an existing TLS server.
Use for certificate renewals when the main TLS server is running.
Registers challenge cert data in a registry for the server’s
KeyManager to serve during ALPN handshakes.
Creates its own registry atom internally.
Use [`challenge-registry`](#challenge-registry) to get the registry for `sni-alpn-ssl-context`.
Returns a solver map with `:present`, `:cleanup`, and `:registry`.
```clojure
(def solver (integrated-solver))
(jetty-ext/sni-alpn-ssl-context lookup-fn (challenge-registry solver))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L159-L195)
---
## switchable-solver
```clojure
(switchable-solver {:keys [port] :or {port 443}})
```
Create a TLS-ALPN-01 solver that can switch from bootstrap to integrated mode.
Returns a solver map with `:present`, `:cleanup`, `:switch-to-integrated!`, and `:registry`.
Pass directly to the automation system’s `:solvers` config.
Creates its own registry atom internally.
Use [`challenge-registry`](#challenge-registry) to get the registry for `sni-alpn-ssl-context`.
Starts in bootstrap mode (starts temporary server for initial cert).
Call [`switch-to-integrated!`](#switch-to-integrated!) after your TLS server starts so renewals
use the integrated solver (registers in registry for KeyManager to serve).
| name | description |
|--------|----------------------------------------|
| `opts` | Options map with `:port` (default 443) |
```clojure
(def solver (switchable-solver {:port 8443}))
;; Use solver with automation
(auto/start {:solvers {:tls-alpn-01 solver} ...})
(auto/manage-domains system ["example.com"])
;; Pass registry to sni-alpn-ssl-context
(jetty-ext/sni-alpn-ssl-context lookup-fn (challenge-registry solver))
;; After TLS server starts
(switch-to-integrated! solver)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L197-L237)
---
## switch-to-integrated!
```clojure
(switch-to-integrated! {:keys [switch-to-integrated!]})
```
Switch a switchable solver from bootstrap to integrated mode.
Call this after your TLS server has started.
Future challenge validations will use the integrated solver
(registers cert data in registry for the KeyManager to serve).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L239-L246)
---
## challenge-registry
```clojure
(challenge-registry {:keys [registry]})
```
Get the challenge registry atom from a solver.
Use this to pass the registry to `sni-alpn-ssl-context` for ALPN challenge support.
```clojure
(def solver (switchable-solver {:port 443}))
(jetty-ext/sni-alpn-ssl-context lookup-fn (challenge-registry solver))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/acme/solver/tls_alpn.clj#L248-L258)
## ol.clave.automation.impl.cache
# ol.clave.automation.impl.cache
In-memory certificate cache for the automation layer.
The cache provides fast certificate lookup for TLS handshakes and
iteration for maintenance loop. Certificates are indexed by SAN
for efficient domain-based lookups.
## cache-certificate
```clojure
(cache-certificate cache_ bundle)
```
Add or update a certificate in the cache.
If `:capacity` is set in the cache and adding would exceed it,
one random certificate is evicted first.
| key | description |
|----------|-----------------------------------------------------|
| `cache_` | Atom containing {:certs {} :index {} :capacity nil} |
| `bundle` | Certificate bundle with :hash and :names |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L44-L70)
---
## lookup-cert
```clojure
(lookup-cert cache_ hostname)
```
Find certificate for hostname.
Tries exact match first, then wildcard match.
| key | description |
|------------|---------------------------------------|
| `cache_` | Atom containing {:certs {} :index {}} |
| `hostname` | Hostname to look up |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L80-L96)
---
## remove-certificate
```clojure
(remove-certificate cache_ bundle)
```
Remove a certificate from the cache.
| key | description |
|----------|-----------------------------------------------------|
| `cache_` | Atom containing {:certs {} :index {} :capacity nil} |
| `bundle` | Certificate bundle with :hash and :names to remove |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L98-L112)
---
## update-ocsp-staple
```clojure
(update-ocsp-staple cache_ hash ocsp-response)
```
Update OCSP staple in existing cached bundle.
| key | description |
|-----------------|-----------------------------------------------------|
| `cache_` | Atom containing {:certs {} :index {} :capacity nil} |
| `hash` | Hash of the certificate to update |
| `ocsp-response` | New OCSP staple data |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L114-L128)
---
## update-ari-data
```clojure
(update-ari-data cache_ hash ari-data)
```
Update ARI data in existing cached bundle.
| key | description |
|------------|---------------------------------------------------------------------|
| `cache_` | Atom containing {:certs {} :index {} :capacity nil} |
| `hash` | Hash of the certificate to update |
| `ari-data` | ARI data with `:suggested-window`, `:selected-time`, `:retry-after` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L130-L143)
---
## mark-managed
```clojure
(mark-managed cache_ hash)
```
Set the :managed flag on a cached bundle.
Used when a previously-cached (unmanaged) certificate becomes managed
via `manage-domains` after passing validation.
| key | description |
|----------|---------------------------------------|
| `cache_` | Atom containing {:certs {} :index {}} |
| `hash` | Hash of the certificate to update |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L145-L160)
---
## newer-than-cache?
```clojure
(newer-than-cache? stored-bundle cached-bundle)
```
Check if a stored certificate is newer than the cached version.
Compares certificates by their `:not-before` timestamp. Returns true
if the stored certificate was issued after the cached one.
| key | description |
|-----------------|---------------------------------|
| `stored-bundle` | Certificate bundle from storage |
| `cached-bundle` | Certificate bundle from cache |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L162-L175)
---
## hash-certificate
```clojure
(hash-certificate cert-chain)
```
Compute a consistent hash of certificate chain bytes.
Uses SHA-256 to produce a unique identifier for a certificate chain.
The hash is stable: same input always produces the same output.
| key | description |
|--------------|----------------------------------------------------------------|
| `cert-chain` | Vector of byte arrays (certificate chain in DER or PEM format) |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L185-L199)
---
## create-bundle
```clojure
(create-bundle certs private-key issuer-key managed?)
```
Create a certificate bundle from ACME response data.
Extracts SANs, computes hash, and creates a complete bundle map
suitable for caching and TLS use.
| key | description |
|---------------|-----------------------------------------------------|
| `certs` | Vector of X509Certificate objects (chain) |
| `private-key` | Private key for the certificate |
| `issuer-key` | Identifier for the issuer (e.g., CA directory host) |
| `managed?` | Whether cert is actively managed for renewal |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L218-L244)
---
## handle-command-result
```clojure
(handle-command-result cache_ cmd result)
```
Update cache based on command result.
Handles cache updates for different command types:
- `:obtain-certificate` success: adds new certificate to cache
- `:renew-certificate` success: removes old cert, adds new cert
- `:fetch-ocsp` success: updates OCSP staple in existing bundle
Does nothing on failure (`:status :error`).
| key | description |
|----------|-----------------------------------------------------|
| `cache_` | Atom containing {:certs {} :index {}} |
| `cmd` | Command descriptor with `:command` and `:bundle` |
| `result` | Result map with `:status` and command-specific data |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/cache.clj#L246-L283)
## ol.clave.automation.impl.config
# ol.clave.automation.impl.config
## resolve-config
```clojure
(resolve-config system domain)
```
Merge global config with per-domain overrides.
Returns the resolved configuration for a specific domain by merging
the global config with any per-domain overrides from config-fn.
If config-fn is nil or returns nil, returns the global config unchanged.
| key | description |
|----------|-----------------------------------------------------------|
| `system` | System map containing `:config` and optional `:config-fn` |
| `domain` | Domain name to resolve configuration for |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L12-L30)
---
## select-issuer
```clojure
(select-issuer config)
```
Select issuers based on the issuer-selection policy.
Returns the issuers in the appropriate order based on `:issuer-selection`:
- `:in-order` (default) - return issuers in original order
- `:shuffle` - return issuers in random order
| key | description |
|----------|----------------------------------------------------------------|
| `config` | Configuration with `:issuers` and optional `:issuer-selection` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L32-L47)
---
## lets-encrypt-production-url
Let’s Encrypt production directory URL.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L49-L51)
---
## default-config
```clojure
(default-config)
```
Returns the default configuration for the automation layer.
Default values:
- Issuer: Let’s Encrypt production
- Key type: P256 (ECDSA)
- OCSP: enabled, must-staple disabled
- ARI: enabled
- Key reuse: disabled
- Cache capacity: unlimited
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L53-L72)
---
## issuer-key-from-url
```clojure
(issuer-key-from-url url)
```
Extract issuer key from directory URL.
Returns a unique identifier for the issuer based on the URL’s host and path.
| key | description |
|-------|--------------------|
| `url` | ACME directory URL |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L92-L116)
---
## cert-storage-key
```clojure
(cert-storage-key issuer-key domain)
```
Generate storage key for a certificate PEM file.
Format: `certificates/{issuer-key}/{domain}/{domain}.crt`
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
| `domain` | Primary domain name |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L118-L130)
---
## key-storage-key
```clojure
(key-storage-key issuer-key domain)
```
Generate storage key for a private key PEM file.
Format: `certificates/{issuer-key}/{domain}/{domain}.key`
| key | description |
|-----|-------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
| `domain` | Primary domain name |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L132-L144)
---
## meta-storage-key
```clojure
(meta-storage-key issuer-key domain)
```
Generate storage key for certificate metadata EDN file.
Format: `certificates/{issuer-key}/{domain}/{domain}.edn`
| key | description |
|-----|-------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
| `domain` | Primary domain name |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L146-L158)
---
## certs-prefix
```clojure
(certs-prefix issuer-key)
```
Generate storage prefix for listing certificates under an issuer.
Format: `certificates/{issuer-key}`
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L160-L169)
---
## account-private-key-storage-key
```clojure
(account-private-key-storage-key issuer-key)
```
Generate storage key for an account private key PEM file.
Format: `accounts/{issuer-key}/account.key`
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L171-L180)
---
## account-public-key-storage-key
```clojure
(account-public-key-storage-key issuer-key)
```
Generate storage key for an account public key PEM file.
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L182-L189)
---
## account-registration-storage-key
```clojure
(account-registration-storage-key issuer-key)
```
Generate storage key for account registration EDN.
Format: `accounts/{issuer-key}/registration.edn`
Contains the account KID (URL) returned by the CA after registration,
allowing subsequent operations to skip the newAccount call.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L191-L199)
---
## ocsp-storage-key
```clojure
(ocsp-storage-key issuer-key domain)
```
Generate storage key for an OCSP staple file.
Format: `certificates/{issuer-key}/{domain}/{domain}.ocsp`
The OCSP staple is stored as raw DER-encoded bytes.
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
| `domain` | Primary domain name |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L201-L215)
---
## compromised-key-storage-key
```clojure
(compromised-key-storage-key domain timestamp)
```
Generate storage key for archiving a compromised private key.
Format: `keys/{domain}.compromised.{timestamp}`
Compromised keys are archived for audit purposes and never reused.
| key | description |
|-------------|----------------------------------------------------|
| `domain` | Primary domain name |
| `timestamp` | ISO-8601 timestamp when key was marked compromised |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L217-L231)
---
## ari-storage-key
```clojure
(ari-storage-key issuer-key domain)
```
Generate storage key for ARI (ACME Renewal Information) data.
Format: `certificates/{issuer-key}/{domain}/{domain}.ari.edn`
The ARI data is stored as EDN containing suggested-window, selected-time,
and retry-after.
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
| `domain` | Primary domain name |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L233-L248)
---
## challenge-token-storage-key
```clojure
(challenge-token-storage-key issuer-key identifier)
```
Generate storage key for a challenge token (distributed solving).
Format: `challenge_tokens/{issuer-key}/{identifier}.edn`
Used to store challenge data so any instance in a cluster can serve
the challenge response for HTTP-01 or TLS-ALPN-01 validation.
| key | description |
|--------------|-------------------------------------------------|
| `issuer-key` | Issuer identifier (hostname from directory URL) |
| `identifier` | Domain or IP address being validated |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L250-L265)
---
## select-chain
```clojure
(select-chain preference chains)
```
Select a certificate chain based on preference.
Preferences:
- `:any` (default) - return first chain offered
- `:shortest` - return chain with fewest certificates
- `{:root "Root CA Name"}` - return chain with matching root name
Returns nil if chains is empty.
Falls back to first chain if root name not found.
| key | description |
|--------------|---------------------------------------------------------------|
| `preference` | Chain preference (`:any`, `:shortest`, or `{:root name}`) |
| `chains` | Sequence of chain maps with `:chain` (certs) and `:root-name` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/config.clj#L267-L299)
## ol.clave.automation.impl.decisions
# ol.clave.automation.impl.decisions
Pure decision functions for the automation layer.
These functions contain all the logic for determining what maintenance
actions are needed for certificates.
## *renewal-threshold*
Fraction of lifetime remaining that triggers renewal.
Default 0.33 means renew when 1/3 of lifetime remains.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L10-L13)
---
## *emergency-override-ari-threshold*
Fraction of lifetime remaining that overrides ARI guidance.
Default 0.05 (1/20) = 5% of lifetime.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L15-L18)
---
## *emergency-critical-threshold*
Fraction of lifetime remaining that triggers critical emergency.
Default 0.02 (1/50) = 2% of lifetime.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L20-L23)
---
## *emergency-min-intervals*
Minimum maintenance intervals before expiration for critical status.
Default 5 intervals.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L25-L28)
---
## *ocsp-refresh-threshold*
Fraction of OCSP validity window that triggers refresh.
Default 0.5 = refresh at 50% of validity window.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L30-L33)
---
## *short-lived-threshold-ms*
Threshold for short-lived certificates (7 days in ms).
Certificates shorter than this use different renewal logic.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L35-L38)
---
## *max-retry-duration-ms*
Maximum duration to retry a failing operation (30 days).
After this duration, the operation fails permanently.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L40-L43)
---
## short-lived-cert?
```clojure
(short-lived-cert? bundle)
```
Check if a certificate is short-lived (< 7 days lifetime).
Short-lived certificates (like those from ACME staging or specialized CAs)
require different renewal timing logic.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L45-L54)
---
## ari-suggests-renewal?
```clojure
(ari-suggests-renewal? bundle now maintenance-interval-ms)
```
Check if ARI data suggests renewal is due.
Per RFC 9710, returns true if current time is past the cutoff, where
cutoff = selected-time minus maintenance-interval. This ensures we don’t
miss the renewal window if maintenance runs just after the selected time.
Returns false if no ARI data or no selected-time is present.
| key | description |
|---------------------------|----------------------------------------------|
| `bundle` | Certificate bundle with optional `:ari-data` |
| `now` | Current instant |
| `maintenance-interval-ms` | Maintenance loop interval in milliseconds |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L56-L76)
---
## calculate-ari-renewal-time
```clojure
(calculate-ari-renewal-time ari-data)
(calculate-ari-renewal-time ari-data rng)
```
Calculate a random time within the ARI suggested renewal window.
| key | description |
|------------|---------------------------------------------------------------|
| `ari-data` | ARI data with `:suggested-window` [start-instant end-instant] |
| `rng` | `java.util.Random` instance for random selection (optional) |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L78-L93)
---
## needs-renewal?
```clojure
(needs-renewal? bundle now maintenance-interval-ms)
```
Check if certificate needs renewal based on expiration, ARI, and emergency.
Returns true if:
- Less than 5% (`**emergency-override-ari-threshold**`) of lifetime remains
(supersedes ARI guidance for safety), OR
- ARI selected-time cutoff has passed (per RFC 9710), OR
- Less than `**renewal-threshold**` (default 1/3) of lifetime remains
The emergency override ensures we never rely solely on ARI when the
certificate is dangerously close to expiration.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L95-L119)
---
## emergency-renewal?
```clojure
(emergency-renewal? bundle now maintenance-interval-ms)
```
Check if certificate is dangerously close to expiration.
Tiered thresholds:
- `:critical` - 1/50 (2%) lifetime remaining OR fewer than 5 maintenance intervals
- `:override-ari` - 1/20 (5%) lifetime remaining, overrides ARI guidance
- `nil` - no emergency
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L121-L144)
---
## needs-ocsp-refresh?
```clojure
(needs-ocsp-refresh? bundle config now)
```
Check if OCSP staple needs refresh.
Returns true if:
- OCSP is enabled in config AND
- Certificate is not short-lived (>= 7 days) AND
- Staple is nil OR past 50% of validity window
Returns false if:
- OCSP is disabled, OR
- Certificate is short-lived (< 7 days lifetime)
Short-lived certificates don’t benefit from OCSP stapling because
the certificate will expire before the OCSP response provides value.
| key | description |
|----------|----------------------------------------------------------|
| `bundle` | Certificate bundle with optional `:ocsp-staple` |
| `config` | Configuration with `:ocsp` map containing `:enabled` key |
| `now` | Current instant |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L169-L196)
---
## check-cert-maintenance
```clojure
(check-cert-maintenance bundle config now maintenance-interval-ms)
```
Returns commands needed for this certificate.
Pure function that examines certificate state and returns a vector
of command descriptors. Does not perform any I/O.
| key | description |
|---------------------------|-------------------------------------------|
| `bundle` | Certificate bundle from cache |
| `config` | Resolved configuration for this domain |
| `now` | Current instant |
| `maintenance-interval-ms` | Maintenance loop interval in milliseconds |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L198-L221)
---
## command-key
```clojure
(command-key cmd)
```
Generate a key for command deduplication.
Returns a vector of [command-type domain] that uniquely identifies
a command. Commands with the same key are considered duplicates
and can be deduplicated by the job queue.
| key | description |
|-------|-------------------------------------------------------|
| `cmd` | Command descriptor with `:command` and `:domain` keys |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L223-L234)
---
## fast-command?
```clojure
(fast-command? cmd)
```
Check if a command is fast.
| key | description |
|-------|----------------------------------------|
| `cmd` | Command descriptor with `:command` key |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L240-L247)
---
## classify-error
```clojure
(classify-error ex)
```
Classify an exception into an error category.
Categories:
- `:network-error` - connection failures, DNS issues, timeouts
- `:rate-limited` - HTTP 429 responses
- `:acme-error` - ACME protocol errors (4xx responses)
- `:server-error` - server-side failures (5xx responses)
- `:config-error` - configuration problems
- `:storage-error` - I/O and storage failures
- `:unknown` - unrecognized exceptions
| key | description |
|------|-----------------------|
| `ex` | Exception to classify |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L261-L302)
---
## retryable-error?
```clojure
(retryable-error? error-type)
```
Check if an error type should be retried.
Retryable errors:
- `:network-error` - transient network issues
- `:rate-limited` - should back off and retry
- `:server-error` - server may recover
- `:storage-error` - storage may become available
Non-retryable errors:
- `:acme-error` - client errors unlikely to succeed
- `:config-error` - configuration must be fixed
- `:unknown` - cannot determine if safe to retry
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L308-L322)
---
## event-for-result
```clojure
(event-for-result cmd result)
```
Create an event from a command result.
Event types by command and status:
- `:obtain-certificate` success -> `:certificate-obtained`
- `:renew-certificate` success -> `:certificate-renewed`
- `:obtain-certificate` error -> `:certificate-failed`
- `:renew-certificate` error -> `:certificate-failed`
- `:fetch-ocsp` success -> `:ocsp-stapled`
- `:fetch-ocsp` error -> `:ocsp-failed`
| key | description |
|----------|----------------------------------------------------|
| `cmd` | Command descriptor with `:command` and `:domain` |
| `result` | Result map with `:status` (`:success` or `:error`) |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L326-L410)
---
## create-certificate-loaded-event
```clojure
(create-certificate-loaded-event bundle)
```
Create an event for a certificate loaded from storage.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L412-L421)
---
## retry-intervals
Exponential backoff intervals for failed operations (in milliseconds).
The pattern is front-loaded: aggressive early retries (1-2 min) catch
transient failures quickly, then backs off through medium intervals
(5-20 min) for rate-limiting or brief outages, then longer intervals
(30 min - 1 hr) for issues requiring propagation or human intervention.
Caps at 6 hours to avoid wasting resources during persistent outages.
Repeated consecutive values control dwell time at each tier before
escalating (e.g., three 10-minute intervals means 3 attempts at that
tier before moving to 20 minutes).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L425-L461)
---
## calculate-maintenance-jitter
```clojure
(calculate-maintenance-jitter maintenance-jitter)
(calculate-maintenance-jitter maintenance-jitter rng)
```
Calculate random jitter for maintenance loop scheduling.
Returns a random value in [0, maintenance-jitter) to spread out
maintenance operations and avoid thundering herd problems.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/decisions.clj#L463-L471)
## ol.clave.automation.impl.domain
# ol.clave.automation.impl.domain
Domain validation for the automation layer.
## validate-domain
```clojure
(validate-domain domain config)
```
Validate a domain name for ACME certificate issuance.
Returns nil if the domain is valid, or an error map if it cannot
receive ACME certificates.
Error map structure:
- `:error` - always `:invalid-domain`
- `:message` - human-readable explanation
Security validations:
- Directory traversal patterns (.., /, \)
- Invalid characters
- Invalid format (leading/trailing dots)
- Wildcard format and DNS-01 solver requirement
- (when using a public ca) the following are not allowed
- localhost, .local, .internal, .test TLDs
- private IP addresses
| key | description |
|----------|---------------------------------|
| `domain` | Domain name to validate |
| `config` | Configuration with :solvers map |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/domain.clj#L100-L156)
## ol.clave.automation.impl.system
# ol.clave.automation.impl.system
System lifecycle and component wiring for the automation layer.
The system map contains all components and is passed to internal functions.
Components access what they need via destructuring.
## *maintenance-interval-ms*
Maintenance loop interval in milliseconds (1 hour).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L36-L38)
---
## *maintenance-jitter-ms*
Maximum jitter for maintenance loop (5 minutes).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L40-L42)
---
## *fast-semaphore-permits*
Concurrent fast command permits (OCSP, ARI).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L44-L46)
---
## *slow-semaphore-permits*
Concurrent slow command permits (obtain, renew).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L48-L50)
---
## *shutdown-timeout-ms*
Timeout for graceful shutdown in milliseconds (30 seconds).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L52-L54)
---
## *config-fn-timeout-ms*
Timeout for config-fn calls in milliseconds (5 seconds).
If config-fn takes longer than this, the domain is skipped.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L56-L59)
---
## submit-command!
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L62-L62)
---
## trigger-maintenance!
```clojure
(trigger-maintenance! system)
```
See [`ol.clave.automation/trigger-maintenance!`](api/ol-clave-automation.adoc#trigger-maintenance-BANG-)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L388-L391)
---
## create
```clojure
(create config)
```
See [`ol.clave.automation/create`](api/ol-clave-automation.adoc#create)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L412-L420)
---
## start!
```clojure
(start! system)
```
See [`ol.clave.automation/start!`](api/ol-clave-automation.adoc#start-BANG-)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L422-L428)
---
## stop
```clojure
(stop system)
```
See [`ol.clave.automation/stop`](api/ol-clave-automation.adoc#stop)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L430-L444)
---
## started?
```clojure
(started? system)
```
See [`ol.clave.automation/started?`](api/ol-clave-automation.adoc#started-QMARK-)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L446-L449)
---
## lookup-cert
```clojure
(lookup-cert system hostname)
```
See [`ol.clave.automation/lookup-cert`](api/ol-clave-automation.adoc#lookup-cert)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L488-L507)
---
## manage-domains
```clojure
(manage-domains system domains)
```
See [`ol.clave.automation/manage-domains`](api/ol-clave-automation.adoc#manage-domains)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1196-L1232)
---
## unmanage-domains
```clojure
(unmanage-domains system domains)
```
See [`ol.clave.automation/unmanage-domains`](api/ol-clave-automation.adoc#unmanage-domains)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1234-L1246)
---
## list-domains
```clojure
(list-domains system)
```
See [`ol.clave.automation/list-domains`](api/ol-clave-automation.adoc#list-domains)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1248-L1257)
---
## get-domain-status
```clojure
(get-domain-status system domain)
```
See [`ol.clave.automation/get-domain-status`](api/ol-clave-automation.adoc#get-domain-status)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1259-L1267)
---
## has-valid-cert?
```clojure
(has-valid-cert? system domain)
```
See [`ol.clave.automation/has-valid-cert?`](api/ol-clave-automation.adoc#has-valid-cert-QMARK-)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1269-L1272)
---
## get-event-queue
```clojure
(get-event-queue system)
```
See [`ol.clave.automation/get-event-queue`](api/ol-clave-automation.adoc#get-event-queue)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1274-L1282)
---
## renew-managed
```clojure
(renew-managed system)
```
See [`ol.clave.automation/renew-managed`](api/ol-clave-automation.adoc#renew-managed)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1284-L1297)
---
## revoke
```clojure
(revoke system certificate opts)
```
See [`ol.clave.automation/revoke`](api/ol-clave-automation.adoc#revoke)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation/impl/system.clj#L1299-L1337)
## ol.clave.automation
# ol.clave.automation
Public API for the ACME certificate automation layer.
The automation layer manages TLS certificate lifecycle automatically:
- Obtains certificates for managed domains
- Renews certificates before expiration
- Handles OCSP stapling
- Provides events for monitoring
## Quick Start
```clojure
(require '[ol.clave.automation :as auto]
'[ol.clave.storage.file :as fs])
;; Create the automation system
(def system (auto/create {:storage (fs/file-storage "/var/lib/acme")
:issuers [{:directory-url "https://acme-v02.api.letsencrypt.org/directory"
:email "admin@example.com"}]
:solvers {:http-01 my-http-solver}}))
;; Optionally get the event queue before starting
(def queue (auto/get-event-queue system))
;; Start the maintenance loop
(auto/start! system)
;; Add domains to manage
(auto/manage-domains system ["example.com"])
;; Look up certificate for TLS handshake
(auto/lookup-cert system "example.com")
;; Stop the system
(auto/stop system)
```
## Configuration
The config map supports:
| key | description |
|---------------------|------------------------------------------------------------------------------|
| `:storage` | Storage implementation (required) |
| `:issuers` | Vector of issuer configs with `:directory-url` and optional `:email`, `:eab` |
| `:issuer-selection` | `:in-order` (default) or `:shuffle` |
| `:key-type` | `:p256` (default), `:p384`, `:rsa2048`, `:rsa4096`, `:rsa8192`, `:ed25519` |
| `:key-reuse` | Reuse private key on renewal (default false) |
| `:solvers` | Map of solver types to implementations |
| `:ocsp` | OCSP config with `:enabled`, `:must-staple` |
| `:ari` | ARI config with `:enabled` |
| `:cache-capacity` | Max certificates in cache (nil = unlimited) |
| `:config-fn` | Function: domain -> config overrides |
| `:http-client` | HTTP client options for ACME requests |
## create
```clojure
(create config)
```
Creates the automation system without starting the maintenance loop.
Returns a system handle that is not yet started.
Throws if configuration is invalid or storage cannot be initialized.
After calling this function you might be interested in [`get-event-queue`](#get-event-queue) and [`start!`](#start!).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L58-L67)
---
## start!
```clojure
(start! system)
```
Starts the maintenance loop on a created system.
Call this after [`create`](#create) to begin automatic certificate management.
Idempotent: calling on an already-started system is a no-op.
Returns the system handle.
See also [`create`](#create).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L69-L79)
---
## stop
```clojure
(stop system)
```
Stops the automation system.
Signals the maintenance loop to stop, waits for in-flight operations,
and releases resources.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L86-L96)
---
## started?
```clojure
(started? system)
```
Returns true if the system is in started state.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L98-L105)
---
## manage-domains
```clojure
(manage-domains system domains)
```
Adds domains to management, triggering immediate certificate obtain.
Returns `nil` on success.
Throws with `:errors` in ex-data if any domain is invalid.
| key | description |
|-----------|----------------------------------|
| `system` | System handle from `start` |
| `domains` | Vector of domain names to manage |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L107-L118)
---
## unmanage-domains
```clojure
(unmanage-domains system domains)
```
Removes domains from management.
Stops renewal and maintenance for these domains.
Certificates remain in storage but are no longer actively managed.
| key | description |
|-----------|------------------------------------|
| `system` | System handle from `start` |
| `domains` | Vector of domain names to unmanage |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L120-L131)
---
## lookup-cert
```clojure
(lookup-cert system hostname)
```
Finds a certificate for a hostname.
Tries exact match first, then wildcard match.
Returns the certificate bundle or nil if not found.
| key | description |
|------------|----------------------------|
| `system` | System handle from `start` |
| `hostname` | Hostname to look up |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L133-L144)
---
## list-domains
```clojure
(list-domains system)
```
Lists all managed domains with status.
Returns a vector of maps with `:domain`, `:status`, and `:not-after`.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L146-L155)
---
## get-domain-status
```clojure
(get-domain-status system domain)
```
Gets detailed status for a specific domain.
Returns a map with `:domain`, `:status`, `:not-after`, `:issuer`,
`:needs-renewal`, or nil if domain is not managed.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
| `domain` | Domain name to check |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L157-L168)
---
## has-valid-cert?
```clojure
(has-valid-cert? system domain)
```
Returns true if the system has a valid certificate for the domain.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
| `domain` | Domain name to check |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L170-L178)
---
## get-event-queue
```clojure
(get-event-queue system)
```
Gets the event queue handle for monitoring.
The queue is created lazily on first call. Subsequent calls return
the same queue instance.
Returns a `java.util.concurrent.LinkedBlockingQueue`
Poll with `.poll`, `.poll(timeout, unit)`, or `.take`.
When the system is stopped via [`stop`](#stop), a `:ol.clave/shutdown` keyword
is placed on the queue. Consumers should check for this sentinel to
know when to stop polling.
| key | description |
|----------|------------------------------|
| `system` | System handle from `start` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L180-L197)
---
## renew-managed
```clojure
(renew-managed system)
```
Forces renewal of all managed certificates.
Submits renewal commands for every managed certificate in the cache.
Commands are submitted asynchronously - this function returns immediately.
Normally certificates are renewed automatically. Use this for
testing or when you need to force renewal.
Returns the number of certificates queued for renewal.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L199-L214)
---
## revoke
```clojure
(revoke system certificate opts)
```
Revokes a certificate.
The `certificate` parameter can be:
- A domain string - looks up the certificate from the cache
- A bundle map - uses the bundle directly
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
| `certificate` | Domain string or bundle map |
| `opts` | Options map (see below) |
Options:
| key | description |
|-----|-------------|
| `:remove-from-storage` | When true, deletes certificate files from storage |
| `:reason` | RFC 5280 revocation reason code (0-6, 8-10) |
Returns `{:status :success}` on successful revocation,
or `{:status :error :message ...}` on failure.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L216-L239)
---
## trigger-maintenance!
```clojure
(trigger-maintenance! system)
```
Manually triggers a maintenance cycle.
This is primarily useful for testing - in normal operation the
maintenance loop runs automatically at regular intervals.
| key | description |
|-----|-------------|
| `system` | System handle from `start` |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/automation.clj#L241-L251)
## ol.clave.certificate.impl.csr
# ol.clave.certificate.impl.csr
Pure Clojure PKCS#10 CSR generation with no external dependencies.
Supports RSA (2048, 3072, 4096), ECDSA (P-256, P-384), and Ed25519.
Automatically handles IDNA conversion for internationalized domains.
Validates and normalizes Subject Alternative Names.
No other extensions or key types are supported.
If you need more features then you will need to use external tools to provide your own CSR.
## create-csr
```clojure
(create-csr key-pair sans & [opts])
```
See [`ol.clave.certificate/csr`](api/ol-clave-certificate.adoc#csr)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/csr.clj#L342-L402)
## ol.clave.certificate.impl.keygen
# ol.clave.certificate.impl.keygen
Key generation utilities for creating keypairs to back TLS certificates.
Supports multiple key types for certificate signing:
- ECDSA curves: P-256 (secp256r1), P-384 (secp384r1)
- EdDSA: Ed25519
- RSA: 2048, 4096, and 8192 bit keys
Note: ACME account keys are managed separately via `ol.clave.impl.crypto`.
This namespace is specifically for certificate keypairs.
## supported-key-types
Set of supported key types for certificate keypair generation.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L22-L29)
---
## pem-encode
```clojure
(pem-encode type der)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L31-L36)
---
## private-key->pem
```clojure
(private-key->pem private-key)
```
See [`ol.clave.certificate/private-key->pem`](api/ol-clave-certificate.adoc#private-key--GT-pem)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L38-L41)
---
## gen-ed25519
```clojure
(gen-ed25519)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L48-L51)
---
## gen-p256
```clojure
(gen-p256)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L53-L57)
---
## gen-p384
```clojure
(gen-p384)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L59-L63)
---
## gen-rsa2048
```clojure
(gen-rsa2048)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L65-L69)
---
## gen-rsa4096
```clojure
(gen-rsa4096)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L71-L75)
---
## gen-rsa8192
```clojure
(gen-rsa8192)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L77-L81)
---
## generate
```clojure
(generate key-type)
```
See [`ol.clave.certificate/keypair`](api/ol-clave-certificate.adoc#keypair)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/keygen.clj#L83-L95)
## ol.clave.certificate.impl.ocsp
# ol.clave.certificate.impl.ocsp
Pure Clojure OCSP (Online Certificate Status Protocol) utilities.
Provides functionality to:
- Extract OCSP responder URLs from certificates
- Fetch OCSP responses from responders
- Parse and validate OCSP responses
## extract-ocsp-urls
```clojure
(extract-ocsp-urls cert)
```
Extract OCSP responder URLs from a certificate’s AIA extension.
Returns a vector of OCSP URLs, or empty vector if none found.
| key | description |
|-----|-------------|
| `cert` | X509Certificate to extract OCSP URLs from |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L61-L90)
---
## create-ocsp-request
```clojure
(create-ocsp-request cert issuer)
```
Create an OCSP request for a certificate.
Requires both the leaf certificate and its issuer certificate.
Returns the DER-encoded OCSP request bytes.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L193-L202)
---
## parse-ocsp-response
```clojure
(parse-ocsp-response response-bytes)
```
Parse an OCSP response and extract status information.
Returns a map with:
- `:status` - One of :good, :revoked, :unknown, or :error
- `:this-update` - When this response was generated
- `:next-update` - When the response expires
- `:revocation-time` - For revoked certs, when it was revoked
- `:revocation-reason` - For revoked certs, the reason code
- `:raw-bytes` - The original DER-encoded response
- `:error-code` - For error responses, the OCSP error code
- `:message` - For error responses, the error message
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L342-L387)
---
## fetch-ocsp-response
```clojure
(fetch-ocsp-response cert issuer responder-url http-opts)
```
Fetch OCSP response for a certificate from the specified responder.
| key | description |
|-----|-------------|
| `cert` | The X509Certificate to check |
| `issuer` | The issuer certificate |
| `responder-url` | URL of the OCSP responder |
| `http-opts` | HTTP client options map |
Returns a result map:
- On success: `{:status :success :ocsp-response {...}}`
- On failure: `{:status :error :message "..."}`
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L391-L428)
---
## fetch-ocsp-for-bundle
```clojure
(fetch-ocsp-for-bundle bundle http-opts responder-overrides)
```
Fetch OCSP response for a certificate bundle.
Extracts the OCSP URL from the leaf certificate and fetches the response.
Supports responder URL overrides for testing.
| key | description |
|-----|-------------|
| `bundle` | Certificate bundle with `:certificate` chain |
| `http-opts` | HTTP client options |
| `responder-overrides` | Optional map of original-url -> override-url |
Returns a result map:
- On success: `{:status :success :ocsp-response {...}}`
- On failure: `{:status :error :message "..."}`
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/ocsp.clj#L430-L468)
## ol.clave.certificate.impl.parse
# ol.clave.certificate.impl.parse
## parse-pem-chain
```clojure
(parse-pem-chain pem)
```
Parse a PEM-encoded certificate chain into structured data.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/parse.clj#L36-L44)
---
## parse-pem-response
```clojure
(parse-pem-response resp url)
```
Validate and parse a PEM certificate response.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/parse.clj#L46-L61)
## ol.clave.certificate.impl.tls-alpn
# ol.clave.certificate.impl.tls-alpn
TLS-ALPN-01 challenge certificate generation.
Builds self-signed X.509 v3 certificates containing the acmeValidationV1
extension required by RFC 8737 for TLS-ALPN-01 ACME challenges.
## acme-tls-1-protocol
ALPN protocol identifier for TLS-ALPN-01 challenges (RFC 8737 Section 6.2).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/tls_alpn.clj#L25-L27)
---
## tlsalpn01-challenge-cert
```clojure
(tlsalpn01-challenge-cert identifier key-authorization)
```
See [`ol.clave.acme.challenge/tlsalpn01-challenge-cert`](api/ol-clave-acme-challenge.adoc#tlsalpn01-challenge-cert)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/tls_alpn.clj#L184-L223)
## ol.clave.certificate.impl.x509
# ol.clave.certificate.impl.x509
X.509 encoding utilities for certificates and CSRs.
We don’t implement all of X.509 (lol), we implement just enough to:
- generate CSRs
- generate TLS-ALPN-01 challenge certificates
Provides:
- IDNA encoding for internationalized domain names in SANs
- GeneralName encoding for Subject Alternative Names
- Extension encoding for certificate extensions
## idna-encode
```clojure
(idna-encode domain)
```
Convert Unicode domain to ASCII (Punycode) using IDNA.
Normalizes to lowercase first, then applies IDNA conversion.
Throws ex-info with ::errors/invalid-idna on failure.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L22-L36)
---
## encode-extension
```clojure
(encode-extension oid critical? value)
```
Encode a single X.509 extension.
Arguments:
oid - OID string (e.g., "2.5.29.17" for subjectAltName)
critical? - Boolean indicating if extension is critical
value - DER-encoded extension value bytes
Returns DER-encoded extension SEQUENCE.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L38-L53)
---
## encode-dns-general-name
```clojure
(encode-dns-general-name domain)
```
Encode DNS GeneralName (context tag 2).
Applies IDNA encoding to the domain before encoding.
Returns DER-encoded GeneralName for DNS identifier.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L55-L62)
---
## encode-ip-general-name
```clojure
(encode-ip-general-name ip-bytes)
```
Encode IP GeneralName (context tag 7) from raw bytes.
Arguments:
ip-bytes - Raw IP address bytes (4 bytes for IPv4, 16 bytes for IPv6)
Returns DER-encoded GeneralName for IP identifier.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate/impl/x509.clj#L64-L72)
## ol.clave.certificate
# ol.clave.certificate
Certificate acquisition and management.
This namespace provides high-level functions for obtaining TLS certificates
from ACME servers, abstracting away the multi-step protocol workflow.
The main entry point is [`obtain`](#obtain) which orchestrates the complete ACME
workflow: creating orders, solving challenges, finalizing with a CSR, and
downloading the issued certificate.
Solvers are maps containing functions that handle challenge provisioning:
- `:present` (required) - provisions resources for ACME validation
- `:cleanup` (required) - removes provisioned resources
- `:wait` (optional) - waits for slow provisioning (e.g., DNS propagation)
- `:payload` (optional) - generates custom challenge response payload
See `ol.clave.solver.http` for an HTTP-01 solver with Ring middleware.
See also `ol.clave.commands` for low-level plumbing operations.
## validate-solvers
```clojure
(validate-solvers solvers)
```
Validate that all solvers have required :present and :cleanup functions.
Solvers are maps that must contain:
- `:present` - function to provision challenge resources
- `:cleanup` - function to clean up provisioned resources
Optional keys are allowed (permissive mode):
- `:wait` - function for slow provisioning operations
- `:payload` - function to generate custom challenge payload
- Any other keys (for user metadata)
Returns nil on success, throws on validation failure.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L38-L63)
---
## wrap-solver-for-distributed
```clojure
(wrap-solver-for-distributed storage issuer-key storage-key-fn solver)
```
Wraps a solver to store/cleanup challenge tokens in shared storage.
This enables distributed challenge solving where multiple instances
behind a load balancer can serve ACME validation responses.
On `:present`: stores challenge data to storage before calling underlying solver.
On `:cleanup`: calls underlying solver cleanup, then deletes from storage.
The stored data is a JSON map containing the full challenge plus key-authorization,
enabling any instance to reconstruct and serve the response.
| key | description |
|-----|-------------|
| `storage` | Storage implementation for persisting challenge tokens |
| `issuer-key` | Issuer identifier for storage key namespacing |
| `storage-key-fn` | Function `(fn [issuer-key identifier]) -> storage-key` |
| `solver` | The underlying solver map to wrap |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L67-L118)
---
## wrap-solvers-for-distributed
```clojure
(wrap-solvers-for-distributed storage issuer-key storage-key-fn solvers)
```
Wraps all solvers in a map for distributed challenge solving.
| key | description |
|-----|-------------|
| `storage` | Storage implementation |
| `issuer-key` | Issuer identifier |
| `storage-key-fn` | Function to generate storage keys |
| `solvers` | Map of challenge-type -> solver |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L120-L134)
---
## lookup-challenge-token
```clojure
(lookup-challenge-token storage issuer-key storage-key-fn identifier)
```
Lookup a stored challenge token from shared storage.
Returns the challenge data map if found, nil otherwise.
| key | description |
|-----|-------------|
| `storage` | Storage implementation |
| `issuer-key` | Issuer identifier |
| `storage-key-fn` | Function to generate storage keys |
| `identifier` | Domain or IP being validated |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L136-L150)
---
## obtain
```clojure
(obtain the-lease session identifiers cert-keypair solvers opts)
```
Obtain a certificate from an ACME server using configured solvers.
This function automates the complete ACME workflow defined in RFC 8555 Section 7.1:
creating an order, solving authorization challenges, finalizing with a CSR,
and downloading the issued certificate.
Parameters:
| name | description |
|---------------|------------------------------------------------------------|
| `the-lease` | Lease for cancellation and timeout control |
| `session` | Authenticated ACME session with account key and KID |
| `identifiers` | Vector of identifier maps from `order/create-identifier` |
| `cert-keypair`| KeyPair for the certificate (distinct from account key) |
| `solvers` | Map of challenge type keyword to solver map |
| `opts` | Optional configuration map |
Options map:
| key | description |
|-------------------------|----------------------------------------------------|
| `:not-before` | Requested validity start (java.time.Instant) |
| `:not-after` | Requested validity end (java.time.Instant) |
| `:profile` | ACME profile name when CA supports profiles |
| `:preferred-challenges` | Vector of challenge types in preference order |
| `:poll-interval-ms` | Polling interval for authorization/order |
| `:poll-timeout-ms` | Polling timeout |
Returns `[updated-session result]` where result is a map:
| key | description |
|----------------|----------------------------------------------------------|
| `:order` | Final order map with status "valid" |
| `:certificates`| Vector of certificate maps |
| `:attempts` | Number of order creation attempts made |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L285-L443)
---
## identifiers-from-sans
```clojure
(identifiers-from-sans sans)
```
Convert a sequence of SAN strings to identifier maps.
Automatically detects IP addresses (IPv4 and IPv6) vs DNS names.
Example:
```clojure
(identifiers-from-sans ["example.com" "192.168.1.1" "2001:db8::1"])
;; => [{:type "dns" :value "example.com"}
;; {:type "ip" :value "192.168.1.1"}
;; {:type "ip" :value "2001:db8::1"}]
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L447-L465)
---
## obtain-for-sans
```clojure
(obtain-for-sans the-lease session sans cert-key solvers)
(obtain-for-sans the-lease session sans cert-key solvers opts)
```
Simplified certificate acquisition for the common case.
Automatically creates identifiers from SAN strings.
Parameters:
| name | description |
|--------------|------------------------------------------------|
| `the-lease` | Lease for cancellation/timeout |
| `session` | Authenticated ACME session |
| `sans` | Vector of SAN strings (domains, IPs) |
| `cert-key` | KeyPair for certificate |
| `solvers` | Map of challenge type keyword to solver map |
| `opts` | Optional configuration map (see [`obtain`](#obtain)) |
Returns `[updated-session result]` as with `obtain-certificate`.
Example:
```clojure
(obtain-certificate-for-sans
(lease/background)
session
["example.com" "www.example.com"]
cert-key
{:http-01 http-solver})
;; With options
(obtain-certificate-for-sans
(lease/background)
session
["example.com"]
cert-key
{:http-01 http-solver :tls-alpn-01 tls-solver}
{:preferred-challenges [:http-01 :tls-alpn-01]})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L467-L506)
---
## csr
```clojure
(csr keypair sans & [opts])
```
Generate a PKCS#10 CSR from a KeyPair
SANs (Subject Alternative Names) are automatically processed:
- Unicode domains are converted to Punycode using IDNA encoding.
- Wildcard usage is validated per RFC 6125 (DNS names only).
- IP address format is validated for both IPv4 and IPv6.
- Values are deduplicated and normalized.
Arguments:
key-pair - java.security.KeyPair (RSA, EC, or EdDSA). Required.
sans - Vector of strings (domain names or IP addresses). Required.
Examples: ["example.com" "*.example.com" "192.0.2.1" "2001:db8::1"]
opts - Map of options. Optional, defaults to {}.
:use-cn? - Boolean. If true, set Subject CN to the first DNS SAN.
Default false. IPs are skipped when searching for CN value.
When false, Subject is empty and all identifiers are in SANs only
(one of three valid options per RFC 8555 Section 7.4).
Returns:
{:csr-pem String - PEM-encoded CSR
:csr-der bytes - Raw DER bytes
:csr-b64url String - Base64URL-encoded DER (no padding) for ACME
:algorithm Keyword - :rsa-2048, :rsa-3072, :rsa-4096, :ec-p256, :ec-p384, or :ed25519
:details Map - Algorithm OIDs, signature info}
Supports RSA (2048, 3072, 4096), ECDSA (P-256, P-384), and Ed25519.
Automatically handles IDNA conversion for internationalized domains.
Validates and normalizes Subject Alternative Names.
No other extensions or key types are supported.
If you need more features then you will need to use external tools to provide your own CSR.
Examples:
;; Modern ACME: no CN in subject (use-cn? = false, the default)
(create-csr kp ["example.com" "*.example.com"])
(create-csr kp ["example.com" "www.example.com"] {})
;; Legacy: CN = first DNS SAN (IPs are skipped)
(create-csr kp ["example.com" "www.example.com"] {:use-cn? true})
;; Mixed DNS and IP SANs (auto-detected)
(create-csr kp ["example.com" "192.0.2.1" "2001:db8::1"])
;; Unicode domains (auto-converted to Punycode)
(create-csr kp ["münchen.example" "www.münchen.example"])
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L508-L555)
---
## private-key->pem
```clojure
(private-key->pem private-key)
```
Encode a private key as PKCS#8 PEM-formatted string.
```clojure
(private-key->pem (.getPrivate keypair))
;; => "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L557-L565)
---
## keypair
```clojure
(keypair)
(keypair key-type)
```
Generate a keypair of the specified type.
`key-type` must be one of [`supported-key-types`](api/ol-clave-certificate-impl-keygen.adoc#supported-key-types):
- `:ed25519` - Ed25519 curve
- `:p256` - ECDSA P-256 (default, recommended)
- `:p384` - ECDSA P-384
- `:rsa2048` - RSA 2048-bit
- `:rsa4096` - RSA 4096-bit
- `:rsa8192` - RSA 8192-bit
If you don’t know which one to choose, just use the default.
Returns a `java.security.KeyPair`.
```clojure
(generate :p256)
;; => #object[java.security.KeyPair ...]
(.getPublic (generate :ed25519))
;; => #object[sun.security.ec.ed.EdDSAPublicKeyImpl ...]
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/certificate.clj#L567-L593)
## ol.clave.crypto.impl.der
# ol.clave.crypto.impl.der
DER (Distinguished Encoding Rules) encoding and decoding for ASN.1 structures.
This namespace provides low-level functions for encoding and decoding ASN.1
data structures according to ITU-T X.690 (DER encoding rules).
We implement just enough to:
- Generate CSRs
- Generate TLS-ALPN-01 challenge certificates
- Parse certificate extensions (AIA, AKI)
- Parse OCSP responses
Encoding functions return byte arrays.
Decoding functions work with byte arrays and return Clojure data structures.
## tag-boolean
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L29-L29)
---
## tag-integer
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L30-L30)
---
## tag-bit-string
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L31-L31)
---
## tag-octet-string
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L32-L32)
---
## tag-null
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L33-L33)
---
## tag-oid
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L34-L34)
---
## tag-enumerated
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L35-L35)
---
## tag-utf8-string
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L36-L36)
---
## tag-printable-string
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L37-L37)
---
## tag-ia5-string
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L38-L38)
---
## tag-utc-time
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L39-L39)
---
## tag-generalized-time
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L40-L40)
---
## tag-sequence
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L41-L41)
---
## tag-set
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L42-L42)
---
## class-context-specific
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L44-L44)
---
## constructed-bit
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L45-L45)
---
## encode-length
```clojure
(encode-length length)
```
Encode DER length octets (short or long form).
Short form (length < 128): single byte with the length value.
Long form (length >= 128): first byte has high bit set and indicates
number of length bytes, followed by length bytes in big-endian order.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L49-L68)
---
## concat-bytes
```clojure
(concat-bytes & arrays)
```
Concatenate multiple byte arrays into a new byte array.
Efficiently copies all input arrays into a single output array.
Returns empty array if no arrays provided.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L70-L83)
---
## der-primitive
```clojure
(der-primitive tag content)
```
Encode DER primitive with given tag and content.
Tag should be a single-byte tag value (e.g., 0x02 for INTEGER).
Content is the raw bytes for this primitive.
Returns complete TLV (tag-length-value) encoded byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L87-L100)
---
## der-constructed
```clojure
(der-constructed tag content)
```
Encode DER constructed with given tag and content.
Tag should include the constructed bit (0x20).
Content is the concatenated encoding of all child elements.
Returns complete TLV (tag-length-value) encoded byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L102-L115)
---
## der-sequence
```clojure
(der-sequence & parts)
```
Encode DER SEQUENCE from parts.
Concatenates all parts and wraps in SEQUENCE tag (0x30).
Returns encoded SEQUENCE byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L119-L126)
---
## der-set
```clojure
(der-set & parts)
```
Encode DER SET from parts.
DER requires SET elements to be sorted in lexicographic order by their
encoded bytes. This implementation sorts all parts before encoding.
Returns encoded SET byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L128-L151)
---
## der-integer
```clojure
(der-integer value)
```
Encode DER INTEGER from long value.
Handles zero specially, uses BigInteger for proper two’s complement encoding.
Returns encoded INTEGER byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L153-L162)
---
## der-integer-bytes
```clojure
(der-integer-bytes value)
```
Encode DER INTEGER from byte array.
Adds leading zero if high bit is set to avoid negative interpretation.
Returns encoded INTEGER byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L164-L175)
---
## der-boolean
```clojure
(der-boolean v)
```
Encode DER BOOLEAN.
True encodes as 0xFF, false as 0x00.
Returns encoded BOOLEAN byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L177-L183)
---
## encode-base128
```clojure
(encode-base128 value)
```
Encode a long value as base-128 (for OID encoding).
Used by der-oid for encoding OID arc values.
Each byte has high bit set except the last byte.
Returns base-128 encoded bytes.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L185-L202)
---
## der-oid
```clojure
(der-oid dotted)
```
Encode DER OBJECT IDENTIFIER from dotted string.
Example: "2.5.4.3" for CN (Common Name).
First two arcs are encoded specially per X.690 (40*arc1 + arc2).
Returns encoded OID byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L204-L223)
---
## der-utf8-string
```clojure
(der-utf8-string s)
```
Encode DER UTF8String.
Encodes string using UTF-8 character set.
Returns encoded UTF8String byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L225-L231)
---
## der-octet-string
```clojure
(der-octet-string content)
```
Encode DER OCTET STRING.
Wraps arbitrary byte content in OCTET STRING tag (0x04).
Returns encoded OCTET STRING byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L233-L239)
---
## der-bit-string
```clojure
(der-bit-string bytes)
```
Encode DER BIT STRING.
First content byte specifies number of unused bits (always 0 here).
Returns encoded BIT STRING byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L241-L250)
---
## der-utc-time
```clojure
(der-utc-time date)
```
Encode DER UTCTime from Date.
Format: YYMMDDHHmmssZ (2-digit year, UTC timezone).
Used for dates before 2050 per X.509 conventions.
Returns encoded UTCTime byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L254-L263)
---
## der-generalized-time
```clojure
(der-generalized-time date)
```
Encode DER GeneralizedTime from Date.
Format: YYYYMMDDHHmmssZ (4-digit year, UTC timezone).
Used for dates in 2050 or later per X.509 conventions.
Returns encoded GeneralizedTime byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L265-L274)
---
## der-context-specific-constructed-implicit
```clojure
(der-context-specific-constructed-implicit tag-number content)
```
Encode IMPLICIT [tag] CONSTRUCTED with given content.
Context-specific tag with constructed bit set (0xA0 | tag-number).
Used for IMPLICIT tagging in ASN.1.
Returns encoded context-specific byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L278-L286)
---
## der-context-specific-primitive
```clojure
(der-context-specific-primitive tag-number raw-content)
```
Encode context-specific PRIMITIVE tag with raw content.
Context-specific tag without constructed bit (0x80 | tag-number).
Used for IMPLICIT tagging of primitive types in ASN.1.
Returns encoded context-specific byte array.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L288-L296)
---
## read-length
```clojure
(read-length data offset)
```
Read DER length and return [length bytes-consumed].
Supports short form (single byte < 128) and long form (multi-byte).
Returns vector of [length-value number-of-bytes-consumed].
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L300-L320)
---
## read-tlv
```clojure
(read-tlv data offset)
```
Read a complete TLV (tag-length-value) structure.
Returns a map with:
- `:tag` - the raw tag byte
- `:tag-class` - :universal, :application, :context-specific, or :private
- `:constructed?` - true if constructed (contains other TLVs)
- `:tag-number` - the tag number within the class
- `:value` - byte array of the value
- `:total-length` - total bytes consumed including tag and length
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L322-L354)
---
## decode-sequence-elements
```clojure
(decode-sequence-elements data)
```
Decode all elements in a SEQUENCE/SET value.
Takes the value bytes (not including SEQUENCE tag/length) and returns
a vector of TLV maps.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L356-L368)
---
## decode-integer
```clojure
(decode-integer value)
```
Decode a DER INTEGER value bytes to BigInteger.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L372-L375)
---
## decode-enumerated
```clojure
(decode-enumerated value)
```
Decode a DER ENUMERATED value bytes to long.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L377-L380)
---
## decode-oid
```clojure
(decode-oid value)
```
Decode a DER OID value bytes to dotted string notation.
First two arcs are encoded as (40 * arc1 + arc2) in the first byte(s).
Subsequent arcs use base-128 encoding with high bit continuation.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L382-L412)
---
## decode-octet-string
```clojure
(decode-octet-string value)
```
Return the raw bytes from an OCTET STRING value.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L414-L417)
---
## decode-bit-string
```clojure
(decode-bit-string value)
```
Decode a BIT STRING value, returning the actual bits as bytes.
First byte indicates unused bits in the last byte.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L419-L430)
---
## decode-ia5-string
```clojure
(decode-ia5-string value)
```
Decode an IA5String (ASCII) value to String.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L432-L435)
---
## decode-utf8-string
```clojure
(decode-utf8-string value)
```
Decode a UTF8String value to String.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L437-L440)
---
## decode-printable-string
```clojure
(decode-printable-string value)
```
Decode a PrintableString value to String.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L442-L445)
---
## decode-generalized-time
```clojure
(decode-generalized-time value)
```
Decode a GeneralizedTime value to java.time.Instant.
Format: YYYYMMDDHHmmss[.fraction]Z
Supports optional fractional seconds.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L458-L469)
---
## decode-utc-time
```clojure
(decode-utc-time value)
```
Decode a UTCTime value to java.time.Instant.
Format: YYMMDDHHmmssZ (2-digit year, interpreted as 1950-2049).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L480-L490)
---
## find-context-tag
```clojure
(find-context-tag elements tag-number)
```
Find a context-specific tagged element by tag number in a sequence of TLVs.
Returns the TLV map if found, nil otherwise.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L494-L501)
---
## unwrap-octet-string
```clojure
(unwrap-octet-string data)
```
Unwrap a DER OCTET STRING and return its content bytes.
Parses the TLV structure and extracts the value.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L503-L511)
---
## unwrap-sequence
```clojure
(unwrap-sequence data)
```
Unwrap a DER SEQUENCE and return its decoded elements.
Parses the TLV structure and decodes all child elements.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/der.clj#L513-L521)
## ol.clave.crypto.impl.json
# ol.clave.crypto.impl.json
## read-str
```clojure
(read-str s)
(read-str s opts)
```
Returns a Clojure value from the JSON string.
Options:
:key-fn - Convert JSON keys using this function. Defaults to keyword.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/json.clj#L5-L12)
---
## write-str
```clojure
(write-str x)
(write-str x opts)
```
Returns a JSON string from the Clojure value.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/json.clj#L14-L18)
## ol.clave.crypto.impl.jwk
# ol.clave.crypto.impl.jwk
## key-algorithm
```clojure
(key-algorithm key)
```
Return :ol.clave.algo/es256 or :ol.clave.algo/ed25519 for supported keys.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L31-L38)
---
## public-jwk
Return the public key as a JWK map.
| Key Type | JWK kty |
|----------|---------|
| EC | "EC" |
| Ed25519 | "OKP" |
| RSA | "RSA" |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L59-L67)
---
## jwk->canonical-json
```clojure
(jwk->canonical-json jwk-map)
```
Render a public JWK map as canonical JSON for JWS embedding.
Fields are sorted alphabetically per RFC 7638.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L98-L119)
---
## jwk-thumbprint-from-jwk
```clojure
(jwk-thumbprint-from-jwk jwk-map)
```
Compute RFC 7638 thumbprint from a JWK map.
Returns base64url-encoded SHA-256 hash of canonical JWK.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L121-L128)
---
## jwk-thumbprint
```clojure
(jwk-thumbprint public-key)
```
Compute RFC 7638 thumbprint for a public key.
Returns base64url-encoded SHA-256 hash of canonical JWK.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jwk.clj#L130-L135)
## ol.clave.crypto.impl.jws
# ol.clave.crypto.impl.jws
## jws-alg
Return the JWS `alg` header value for a key.
| Key Type | Result |
|----------|-----------|
| P-256 | "ES256" |
| P-384 | "ES384" |
| Ed25519 | "EdDSA" |
| RSA | "RS256" |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L123-L132)
---
## sign
Sign data bytes, return signature bytes in JWS format.
For ECDSA, returns R||S concatenated (not DER).
For EdDSA and RSA, returns raw signature bytes.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L148-L153)
---
## protected-header-json
```clojure
(protected-header-json alg kid nonce url jwk-json)
```
Construct the protected header JSON string with deterministic field order.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L194-L218)
---
## final-jws-json
```clojure
(final-jws-json protected-b64 payload-b64 signature-b64)
```
Assemble the final JWS JSON object with deterministic ordering.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L220-L225)
---
## protected-dot-payload-bytes
```clojure
(protected-dot-payload-bytes protected-b64 payload-b64)
```
Return ASCII bytes of '<protected>.<payload>'.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L227-L230)
---
## encode-payload-b64
```clojure
(encode-payload-b64 payload-json)
```
Base64url-encode the payload JSON string or return the empty string when nil.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L232-L239)
---
## encode-protected-b64
```clojure
(encode-protected-b64 alg kid nonce url jwk-json)
```
Construct and base64url-encode the protected header JSON.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L241-L245)
---
## encode-signature-b64
```clojure
(encode-signature-b64 alg private-key-or-mac protected-dot-payload)
```
Compute the signature for the given alg and return base64url-encoded value.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L247-L256)
---
## jws-encode-json
```clojure
(jws-encode-json payload-json keypair kid nonce url)
```
Build a JSON-serialized JWS object.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L281-L291)
---
## jws-encode-eab
```clojure
(jws-encode-eab account-key-or-keypair mac-key kid url)
```
Construct an External Account Binding JWS per RFC 8555 section 7.3.4.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/jws.clj#L293-L306)
## ol.clave.crypto.impl.parse-ip
# ol.clave.crypto.impl.parse-ip
Parse a string representation of an IPv4 or IPv6 address into an InetAddress instance.
Unlike `InetAddress/getByName`, the functions in this namespace never cause
DNS services to be accessed. This avoids potentially blocking/side-effecting
network calls that can occur when using the JDK’s built-in methods to parse IP
addresses.
This implementation is inspired by Google Guava’s InetAddresses class, which
provides similar functionality in Java. This code focuses on strict validation
of IP address formats according to relevant RFCs.
Features:
- Non-blocking IP address parsing with pure functions
- Strict RFC-compliant validation
- Support for all IPv4 formats
- Support for all IPv6 formats (including compressed notation and embedded IPv4)
- Support for IPv6 scope identifiers
The main entry point is `parse-ip`, which takes a string and returns an
InetAddress instance or nil if the input is an invalid ip address literal.
## ip-string->bytes
```clojure
(ip-string->bytes ip-string)
```
Convert an IP string to bytes. Returns [byte-array scope-id] or nil if invalid.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/parse_ip.clj#L204-L225)
---
## bytes->inet-address
```clojure
(bytes->inet-address addr)
(bytes->inet-address addr scope)
```
Convert a byte array into an InetAddress.
Args:
addr: the raw 4-byte or 16-byte IP address in big-endian order
scope: optional scope identifier for IPv6 addresses
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/parse_ip.clj#L235-L260)
---
## from-string
```clojure
(from-string ip-string)
```
Returns the InetAddress having the given string representation or nil otherwise.
This function parses IP address strings without performing DNS lookups, making it
suitable for environments where DNS lookups would cause unwanted blocking or side effects.
It supports:
- IPv4 addresses in dotted decimal format (e.g., "192.168.1.1")
- IPv6 addresses in hex format with optional compression (e.g., "2001:db8::1")
- IPv6 addresses with scope IDs (e.g., "fe80::1%eth0" or "fe80::1%1")
- IPv6 addresses with embedded IPv4 (e.g., "::ffff:192.168.1.1")
If the input is already an InetAddress, it is returned unchanged.
Args:
ip-string: A string representing an IP address or an InetAddress
Returns:
An InetAddress object, or nil if the input couldn’t be parsed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/parse_ip.clj#L262-L294)
## ol.clave.crypto.impl.util
# ol.clave.crypto.impl.util
## qualify-keys
```clojure
(qualify-keys ns-prefix m)
```
Qualify the top-level keys of map `m` into `ns-prefix`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/crypto/impl/util.clj#L5-L17)
## ol.clave.errors
# ol.clave.errors
Shared error keyword definitions and helpers for ex-info payloads.
## unsupported-key
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L4-L4)
---
## invalid-header
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L5-L5)
---
## invalid-eab
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L6-L6)
---
## signing-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L7-L7)
---
## ecdsa-signature-format
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L8-L8)
---
## json-escape
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L9-L9)
---
## base64
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L10-L10)
---
## invalid-account-edn
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L12-L12)
---
## invalid-account
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L13-L13)
---
## invalid-contact
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L14-L14)
---
## invalid-contact-entry
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L15-L15)
---
## invalid-contact-uri
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L16-L16)
---
## invalid-tos
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L17-L17)
---
## invalid-directory
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L19-L19)
---
## account-creation-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L21-L21)
---
## missing-location-header
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L22-L22)
---
## cancelled
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L24-L24)
---
## timeout
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L25-L25)
---
## invalid-scope
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L26-L26)
---
## account-retrieval-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L29-L29)
---
## account-update-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L30-L30)
---
## account-deactivation-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L31-L31)
---
## external-account-required
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L32-L32)
---
## unauthorized-account
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L33-L33)
---
## missing-account-context
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L34-L34)
---
## invalid-account-key
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L35-L35)
---
## account-key-rollover-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L36-L36)
---
## account-key-rollover-verification-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L37-L37)
---
## account-not-found
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L38-L38)
---
## invalid-san
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L41-L41)
---
## invalid-idna
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L42-L42)
---
## invalid-ip
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L43-L43)
---
## encoding-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L44-L44)
---
## unsupported-identifier
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L47-L47)
---
## malformed-pem
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L49-L49)
---
## key-mismatch
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L50-L50)
---
## problem
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L53-L53)
---
## server-error
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L55-L55)
---
## value-too-large
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L57-L57)
---
## order-creation-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L60-L60)
---
## order-retrieval-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L61-L61)
---
## order-not-ready
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L62-L62)
---
## order-invalid
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L63-L63)
---
## order-timeout
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L64-L64)
---
## order-inconsistent
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L65-L65)
---
## authorization-retrieval-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L68-L68)
---
## authorization-invalid
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L69-L69)
---
## authorization-unusable
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L70-L70)
---
## authorization-timeout
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L71-L71)
---
## challenge-rejected
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L72-L72)
---
## no-compatible-challenge
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L73-L73)
---
## invalid-solver
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L76-L76)
---
## pre-authorization-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L79-L79)
---
## pre-authorization-unsupported
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L80-L80)
---
## wildcard-identifier-not-allowed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L81-L81)
---
## certificate-download-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L84-L84)
---
## unexpected-content-type
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L85-L85)
---
## revocation-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L88-L88)
---
## invalid-certificate
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L89-L89)
---
## renewal-info-failed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L92-L92)
---
## renewal-info-invalid
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L93-L93)
---
## problem-type-ns
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L101-L101)
---
## pt-account-does-not-exist
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L103-L103)
---
## pt-already-revoked
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L104-L104)
---
## pt-bad-csr
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L105-L105)
---
## pt-bad-nonce
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L106-L106)
---
## pt-bad-public-key
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L107-L107)
---
## pt-bad-revocation-reason
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L108-L108)
---
## pt-bad-signature-algorithm
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L109-L109)
---
## pt-caa
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L110-L110)
---
## pt-compound
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L111-L111)
---
## pt-connection
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L112-L112)
---
## pt-dns
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L113-L113)
---
## pt-external-account-required
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L114-L114)
---
## pt-incorrect-response
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L115-L115)
---
## pt-invalid-contact
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L116-L116)
---
## pt-malformed
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L117-L117)
---
## pt-order-not-ready
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L118-L118)
---
## pt-rate-limited
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L119-L119)
---
## pt-rejected-identifier
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L120-L120)
---
## pt-server-internal
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L121-L121)
---
## pt-tls
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L122-L122)
---
## pt-unauthorized
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L123-L123)
---
## pt-unsupported-contact
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L124-L124)
---
## pt-unsupported-identifier
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L125-L125)
---
## pt-user-action-required
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L126-L126)
---
## pt-already-replaced
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L128-L128)
---
## failed-identifiers
```clojure
(failed-identifiers problem)
```
Extract identifiers from problem subproblems.
Returns vector of identifier maps, e.g., `[{:type "dns" :value "example.com"}]`.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L134-L138)
---
## subproblem-for
```clojure
(subproblem-for problem identifier)
```
Find subproblem for specific identifier.
Returns the first subproblem matching the given identifier, or nil.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L140-L144)
---
## ex
```clojure
(ex type message data)
(ex type message data cause)
```
Convenience wrapper for ex-info that associates the shared :type key.
Usage: (errors/ex errors/invalid-header "message" {:field :kid :reason "missing"})
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/errors.clj#L146-L152)
## ol.clave.ext.common
# ol.clave.ext.common
Common utilities for clave server extensions.
This namespace provides server-agnostic helpers for working with
clave’s automation layer, including keystore creation and event processing.
These functions can be used by any server extension (Jetty, http-kit, etc.).
## create-keystore
```clojure
(create-keystore bundle)
(create-keystore bundle password)
```
Create an in-memory PKCS12 KeyStore from a clave certificate bundle.
No disk I/O - purely in-memory operation suitable for TLS handshakes.
| key | description |
|------------|-------------------------------------------------------------|
| `bundle` | Certificate bundle from [`ol.clave.automation/lookup-cert`](api/ol-clave-automation.adoc#lookup-cert) |
| `password` | Optional keystore password (default "changeit") |
Returns a `java.security.KeyStore` ready for use with TLS servers.
Returns nil if bundle is nil (no certificate available yet).
```clojure
(create-keystore (auto/lookup-cert system "example.com"))
;; => #object[java.security.KeyStore ...]
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L15-L44)
---
## certificate-event?
```clojure
(certificate-event? evt)
```
Check if an event indicates a certificate change.
Returns true for `:certificate-obtained`, `:certificate-renewed`, and
`:certificate-loaded` events.
| key | description |
|-------|----------------------------------------------------|
| `evt` | Event from [`ol.clave.automation/get-event-queue`](api/ol-clave-automation.adoc#get-event-queue) |
```clojure
(when (certificate-event? evt)
(log/info "Certificate updated for" (event-domain evt)))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L46-L62)
---
## event-domain
```clojure
(event-domain evt)
```
Extract the domain name from a certificate event.
Returns the domain string or nil if event has no domain.
| key | description |
|-------|-------------|
| `evt` | Event map |
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L64-L73)
---
## wrap-redirect-https
```clojure
(wrap-redirect-https handler)
(wrap-redirect-https handler {:keys [ssl-port] :or {ssl-port 443}})
```
Ring middleware that redirects HTTP requests to HTTPS.
| key | description |
|------------|----------------------------------------------------|
| `handler` | Ring handler to wrap |
| `opts` | Options map with `:ssl-port` |
Options:
- `:ssl-port` - HTTPS port for redirect URL.
Defaults to 443 (implicit, no port in URL).
Use a custom port like 8443 to include it explicitly.
Passes through requests that are already HTTPS (by `:scheme` or `x-forwarded-proto` header).
```clojure
(wrap-redirect-https handler {:ssl-port 8443})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L75-L106)
---
## no-op-solver
```clojure
(no-op-solver)
```
Create a no-op ACME solver for testing.
Returns a solver that does nothing.
Useful with `PEBBLE_VA_ALWAYS_VALID=1` where challenge validation is skipped.
```clojure
{:solvers {:http-01 (no-op-solver)}}
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L108-L119)
---
## wait-for-certificates
```clojure
(wait-for-certificates system domains)
```
Wait for certificates to be available for all domains.
Polls [`ol.clave.automation/lookup-cert`](api/ol-clave-automation.adoc#lookup-cert) once per second until certificates
are available for every domain. Blocks indefinitely until the automation
system obtains all certificates or throws an error.
| key | description |
|-----------|---------------------------------|
| `system` | clave automation system |
| `domains` | Vector of domains to wait for |
Returns nil once all certificates are available.
```clojure
(wait-for-certificate system ["example.com" "www.example.com"])
(create-keystore (auto/lookup-cert system "example.com"))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/common.clj#L121-L146)
## ol.clave.ext.jetty
# ol.clave.ext.jetty
Jetty integration for clave automation.
Provides SNI-based certificate selection during TLS handshakes.
Certificates are looked up on-demand for each connection based on the
requested hostname, so renewals take effect immediately.
This ns is useful for authors integrating with jetty directly. If you want to use a jetty ring adapter see:
* [`ol.clave.ext.ring-jetty-adapter`](api/ol-clave-ext-ring-jetty-adapter.adoc)
## sni-key-manager
```clojure
(sni-key-manager lookup-fn)
```
Create an X509ExtendedKeyManager that looks up certificates by SNI hostname.
On each TLS handshake, extracts the SNI hostname from the ClientHello and
calls `lookup-fn` to retrieve the certificate bundle for that hostname.
| key | description |
|-------------|---------------------------------------------------------|
| `lookup-fn` | Function `(fn [hostname] bundle)` returning cert bundle |
The `lookup-fn` receives the SNI hostname and should return a certificate
bundle map with `:certificate` (vector of X509Certificate) and `:private-key`.
Returns nil if no certificate is available for that hostname.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L34-L85)
---
## sni-ssl-context
```clojure
(sni-ssl-context lookup-fn)
```
Create an SSLContext configured with SNI-aware certificate selection.
Uses `lookup-fn` to fetch certificates during TLS handshake based on
the client’s requested hostname (SNI). Certificates are looked up fresh
on each handshake, so renewals take effect immediately.
| key | description |
|-------------|---------------------------------------------------------|
| `lookup-fn` | Function `(fn [hostname] bundle)` returning cert bundle |
Returns a `javax.net.ssl.SSLContext` ready for use with Jetty.
```clojure
(def ssl-ctx (sni-ssl-context
(fn [hostname] (auto/lookup-cert system hostname))))
(jetty/run-jetty handler {:ssl-context ssl-ctx ...})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L87-L112)
---
## sni-alpn-key-manager
```clojure
(sni-alpn-key-manager lookup-fn challenge-registry)
```
Create an X509ExtendedKeyManager that handles both SNI and ALPN challenges.
Extends the SNI-based certificate selection with TLS-ALPN-01 challenge support.
During TLS handshake:
1. If ALPN protocol is 'acme-tls/1' and challenge-registry has data, serve challenge cert
2. Otherwise, use lookup-fn for normal SNI-based cert selection
This does NOT interfere with HTTP/2 (h2) negotiation because:
- Regular clients offer ["h2", "http/1.1"] -> normal cert via SNI lookup
- ACME servers offer ["acme-tls/1"] exclusively -> challenge cert
| key | description |
|----------------------|---------------------------------------------------|
| `lookup-fn` | Function `(fn [hostname] bundle)` for SNI lookup |
| `challenge-registry` | Atom with domain->challenge-cert-data map |
The challenge-registry contains maps as returned by `tlsalpn01-challenge-cert`:
- `:x509` - The X509Certificate to serve
- `:keypair` - The KeyPair with private key
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L124-L202)
---
## sni-alpn-ssl-context
```clojure
(sni-alpn-ssl-context lookup-fn tls-alpn-solver)
```
Create an SSLContext with SNI cert selection and ALPN challenge support.
For normal HTTPS traffic: looks up certs by SNI hostname via lookup-fn.
For ACME TLS-ALPN-01 challenges: serves challenge cert when ALPN is 'acme-tls/1'.
| key | description |
|-------------------|--------------------------------------------------|
| `lookup-fn` | Function `(fn [hostname] bundle)` for SNI lookup |
| `tls-alpn-solver` | TLS-ALPN solver with `:registry` key ) |
Returns a `javax.net.ssl.SSLContext` ready for use with Jetty.
```clojure
(def tls-alpn-solver (tls-alpn/switchable-solver {:port 443}))
(def ssl-ctx (sni-alpn-ssl-context
(fn [hostname] (auto/lookup-cert system hostname))
tls-alpn-solver))
(jetty/run-jetty handler {:ssl-context ssl-ctx ...})
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/jetty.clj#L204-L231)
## ol.clave.ext.ring-jetty-adapter
# ol.clave.ext.ring-jetty-adapter
Ring Jetty adapter integration for clave automation.
Provides a high-level API for running Jetty with auto-renewing TLS certificates.
Wraps ring-jetty-adapter with the same `[handler opts]` signature.
Automatically configures both HTTP-01 and TLS-ALPN-01 solvers.
Uses SNI-based certificate selection: certificates are looked up fresh on each
TLS handshake, so renewals take effect immediately without server restart.
```clojure
(require '[ol.clave.ext.ring-jetty-adapter :as clave-jetty])
(def ctx (clave-jetty/run-jetty handler
{:port 80
:ssl-port 443
::clave-jetty/config
{:storage (file-storage/file-storage "/tmp/certs")
:issuers [{:directory-url "https://acme-v02.api.letsencrypt.org/directory"
:email "admin@example.com"}]
:domains ["example.com"]}}))
;; Later: stop everything
(clave-jetty/stop ctx)
```
## run-jetty
```clojure
(run-jetty handler {::keys [config] :as opts})
```
Serve `handler` over HTTPS for all `domains` with automatic certificate management.
This is an opinionated, high-level convenience function that applies sane
defaults for production use: challenge solving, HTTP to HTTPS redirects,
and SNI-based certificate selection.
Blocks until the initial certificate is obtained, then starts serving.
Redirects all HTTP requests to HTTPS (when HTTP port is configured).
Obtains and renews TLS certificates automatically.
Certificate renewals take effect immediately via SNI-based selection.
For advanced use cases, use `ring.adapter.jetty/run-jetty` directly with
[`ol.clave.ext.jetty`](api/ol-clave-ext-jetty.adoc) functions for certificate management.
`opts` are passed through to `ring.adapter.jetty/run-jetty`.
Exception: the `:join?` option from `ring.adapter.jetty/run-jetty` is not supported.
Use [`stop`](#stop) to shut down the server instead.
Calling this function signifies acceptance of the CA’s Subscriber Agreement
and/or Terms of Service.
| key | description |
|-----------|-------------------------------------------------------------------------------|
| `handler` | Ring handler |
| `opts` | Options map, see `ring.adapter.jetty/run-jetty` for jetty-adapter’s options |
Clave config is provided via the `:ol.clave.ext.ring-jetty-adapter/config` key in `opts`:
| key | description | default |
|-------------------|-----------------------------------------|-----------|
| `:domains` | Domains to manage certs for (required) | |
| `:redirect-http?` | Wrap handler with HTTP->HTTPS redirect | true |
Additional automation config keys (e.g., `:issuers`, `:storage` etc.) are passed through to [`ol.clave.automation/create`](api/ol-clave-automation.adoc#create).
Returns a context map for use with [`stop`](#stop).
```clojure
(def server (run-jetty handler
{:port 80 :ssl-port 443
::config {:domains ["example.com"]}}))
;; Later:
(stop server)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/ring_jetty_adapter.clj#L45-L119)
---
## stop
```clojure
(stop {:keys [server system]})
```
Stop a server context returned by [`run-jetty`](#run-jetty).
Stops the Jetty server and automation system.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/ext/ring_jetty_adapter.clj#L121-L127)
## ol.clave.lease
# ol.clave.lease
Cooperative cancellation and deadline propagation for concurrent operations.
A `lease` represents a bounded lifetime for work.
It carries an optional deadline, tracks cancellation state, and provides a
completion signal that callers can poll or block on.
## Motivation
Individual timeout parameters scattered across function calls lead to
inconsistent budgets and subtle hangs.
A `lease` provides one coherent boundary: the caller defines the maximum time
for an entire operation, and all nested work inherits that constraint.
The `lease` is intentionally minimal and does not prescribe any particulary
concurrency model. Virtual threads, core.async go-loops, Java
StructuredTaskScopes, etc all work equally well with leases.
This design enables leases to serve as common ground between libraries with
different runtime preferences. A library using one concurrency model can
accept the same lease type as a library using another, allowing callers to
compose them without adapter layers or runtime coupling.
## Cooperative Cancellation
Cancellation are entirely advisory.
A `lease` does not forcibly terminate anything. It merely records that
cancellation has been requested and notifies interested parties.
The lease transitions from `:active` to `:cancelled` or `:timed-out`, but
running code continues unless it explicitly checks the lease and decides to
stop.
This cooperative model requires discipline from functions that receive a
lease: they must periodically consult the lease and honor cancellation
promptly.
The tradeoff is predictability.
Code always controls its own teardown, resources are released in an orderly
fashion, and there are no surprising interruptions mid-operation.
## Concepts
Lease: A value representing cancellation state (`:active`, `:cancelled`, or
`:timed-out`), an optional deadline (monotonic nanoTime), and parent-child
relationships for propagation.
Deadline vs timeout: A timeout is a duration ("at most 2 seconds"); a
deadline is an absolute monotonic bound ("stop at nanoTime T").
This library accepts either via [`with-timeout`](#with-timeout) and [`with-deadline`](#with-deadline), but
internally tracks a single deadline per lease.
Deadlines use `System/nanoTime` for monotonic timing, immune to wall-clock
changes (NTP adjustments, manual clock changes, VM suspend/resume).
## Honoring a Lease
When your function receives a lease as an argument, you are accepting
responsibility to respect it.
The caller trusts that if they cancel the lease, your function will notice
and stop work promptly and correctly.
At a minimum, check the lease before starting expensive operations.
Call [`active?!`](#active?!) at the top of your function and at natural checkpoints: the
start of each loop iteration, before issuing a network request, or after
returning from a potentially slow sub-call.
If the lease has been cancelled, [`active?!`](#active?!) throws an exception containing
the cancellation cause, which unwinds the stack cleanly.
For non-throwing checks, use [`active?`](#active?) and return early or break out of
loops when it returns false.
The choice between throwing and returning depends on your error-handling
style, but the principle is the same: stop doing new work once the lease is
no longer active.
When calling other lease-aware functions, pass the lease through so they
inherit the same cancellation boundary.
If you need to spawn concurrent work, derive child leases with [`with-cancel`](#with-cancel)
or [`with-timeout`](#with-timeout) and cancel them in a finally block to ensure cleanup.
The goal is prompt, graceful termination without leaking resources.
A well-behaved function notices cancellation within a reasonable window
(tens to hundreds of milliseconds for most operations) and exits without
leaving resources dangling or work half-done.
## Parent-Child Relationships
Derived leases form a tree.
When a parent cancels, all descendants cancel automatically.
Children never cancel parents.
Child deadlines adopt the earliest of the parent deadline and any explicitly
provided deadline.
## Thread Safety
Leases are safe for concurrent use.
Multiple threads may read state, register listeners, or attempt cancellation.
Only the first cancellation wins; subsequent calls are no-ops.
## Usage
```clojure
(require '[ol.clave.lease :as l])
;; Create a root lease and derive a child with timeout
(let [[lease cancel] (l/with-timeout (l/background) 5000)]
(try
(do-work lease)
(finally
(cancel))))
;; Check cancellation state
(when (l/active? lease)
(continue-work))
;; Block until cancelled
(deref (l/done-signal lease))
```
See also: [`with-cancel`](#with-cancel), [`with-timeout`](#with-timeout), [`with-deadline`](#with-deadline), [`active?!`](#active?!)
## ILease
Protocol for cooperative cancellation and deadline tracking.
All methods are non-blocking and safe for concurrent use from multiple
threads.
The [`done-signal`](#done-signal) method returns a derefable; blocking occurs only when
dereferencing that signal.
_protocol_
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L125-L162)
### deadline
```clojure
(deadline lease)
```
Returns the deadline as a monotonic nanoTime (Long), or `nil` if no deadline.
The value is from `System/nanoTime` and is only meaningful for comparison
with other nanoTime values.
Use <> to get a human-readable `Duration` until expiry.
---
### done-signal
```clojure
(done-signal lease)
```
Returns a read-only derefable that yields `true` when `lease` ends.
Use `deref` with a timeout to wait for cancellation, or
`clojure.core/realized?` for a non-blocking check.
[source,clojure]
```
;; Block with timeout
(deref (done-signal lease) 1000 :still-active)
;; Non-blocking check
(realized? (done-signal lease))
```
---
### cause
```clojure
(cause lease)
```
Returns the `Throwable` that caused cancellation, or `nil` if `lease` is active.
The cause contains `:type` in its `ex-data`:
- `:lease/cancelled` for explicit cancellation
- `:lease/deadline-exceeded` for timeout
---
### active?
```clojure
(active? lease)
```
Returns `true` when `lease` has not been cancelled or timed out.
---
## background
```clojure
(background)
```
Creates a root lease with no deadline or parent.
The background lease is never cancelled on its own; it serves as the ancestor
for all derived leases in an operation tree.
Use [`with-cancel`](#with-cancel), [`with-timeout`](#with-timeout), or [`with-deadline`](#with-deadline) to derive child
leases with cancellation or deadline constraints.
```clojure
(let [root (background)
[child cancel] (with-timeout root 5000)]
(try
(do-work child)
(finally
(cancel))))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L335-L352)
---
## with-cancel
```clojure
(with-cancel parent)
```
Derives a cancellable child lease from `parent`.
Returns `[child cancel-fn]` where:
- `child` is the derived lease, inheriting `parent’s deadline
- `cancel-fn` cancels `child` and all its descendants
The cancel function accepts an optional cause argument.
Without arguments, it uses a generic `:lease/cancelled` exception.
Calling cancel multiple times is safe; only the first call takes effect.
When `parent` cancels, `child` cancels automatically with the same cause.
```clojure
(let [[lease cancel] (with-cancel parent)]
(try
(do-work lease)
(finally
(cancel))))
;; Cancel with custom cause
(cancel (ex-info "user abort" {:reason :user-request}))
```
See also: [`with-timeout`](#with-timeout), [`with-deadline`](#with-deadline), [`background`](#background)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L354-L388)
---
## with-deadline
```clojure
(with-deadline parent dl)
```
Derives a child lease from `parent` with an absolute monotonic deadline.
Returns `[child cancel-fn]` where `child` will automatically cancel with
`:lease/deadline-exceeded` when the deadline passes.
`dl` is a monotonic nanoTime value from `System/nanoTime`.
The effective deadline is the earlier of `parent’s deadline and `dl`.
If `dl` has already passed, `child` is cancelled immediately.
Most callers should prefer [`with-timeout`](#with-timeout) which accepts human-friendly
Duration or milliseconds.
```clojure
;; 30 second deadline using nanoTime
(let [dl (+ (System/nanoTime) (* 30 1000000000))
[lease cancel] (l/with-deadline parent dl)]
(try
(do-work lease)
(finally
(cancel))))
```
See also: [`with-timeout`](#with-timeout), [`with-cancel`](#with-cancel), [`deadline`](#deadline)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L390-L424)
---
## with-timeout
```clojure
(with-timeout parent timeout)
```
Derives a child lease from `parent` with a relative timeout.
Returns `[child cancel-fn]` where `child` will automatically cancel with
`:lease/deadline-exceeded` after `timeout` elapses.
`timeout` may be:
- `java.time.Duration` for precise control
- Long/integer for milliseconds
The effective deadline is computed as `(now + timeout)` and combined with
the parent deadline using earliest-wins semantics.
A zero or negative timeout cancels the child immediately.
```clojure
;; 5 second timeout
(let [[lease cancel] (with-timeout parent 5000)]
(try
(do-work lease)
(finally
(cancel))))
;; Using Duration
(with-timeout parent (Duration/ofSeconds 30))
```
See also: [`with-deadline`](#with-deadline), [`with-cancel`](#with-cancel)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L426-L454)
---
## active?!
```clojure
(active?! lease)
```
Returns `lease` if active, otherwise throws the cancellation cause.
Use this for explicit cancellation checks that should fail fast.
The thrown exception is the same value returned by [`cause`](#cause), containing
`:type` of either `:lease/cancelled` or `:lease/deadline-exceeded`.
```clojure
;; Check and continue
(active?! lease)
(do-next-step)
;; In a loop
(loop []
(active?! lease)
(when (more-work?)
(process-item)
(recur)))
```
See also: [`active?`](#active?), [`cause`](#cause)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L456-L480)
---
## remaining
```clojure
(remaining lease)
```
Returns the time remaining until `lease` expires as a `java.time.Duration`.
Returns `nil` if the lease has no deadline.
Returns `Duration/ZERO` if the deadline has already passed.
```clojure
(when-let [dur (l/remaining lease)]
(println "Time left:" (.toMillis dur) "ms"))
```
See also: [`deadline`](#deadline), [`active?`](#active?)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L482-L499)
---
## sleep
```clojure
(sleep lease ms)
```
Cooperatively wait for `ms` milliseconds or until `lease` ends.
Returns `:slept` if the full duration elapsed, or `:lease-ended` if the
lease was cancelled or timed out during the wait.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/lease.clj#L501-L512)
## ol.clave.specs
# ol.clave.specs
## new-nonce-url
```clojure
(new-nonce-url session)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L91-L92)
---
## new-account-url
```clojure
(new-account-url session)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L94-L95)
---
## new-order-url
```clojure
(new-order-url session)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L97-L98)
---
## key-change-url
```clojure
(key-change-url session)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L100-L101)
---
## new-authz-url
```clojure
(new-authz-url session)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L103-L104)
---
## identifier?
```clojure
(identifier? value)
```
Return true when `value` conforms to an ACME identifier map.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L139-L142)
---
## order-url
```clojure
(order-url order)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L164-L165)
---
## certificate-url
```clojure
(certificate-url order)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/specs.clj#L167-L168)
## ol.clave.storage.file
# ol.clave.storage.file
Filesystem-based storage implementation.
Provides a [`FileStorage`](#filestorage) record that implements [`ol.clave.storage/Storage`](api/ol-clave-storage.adoc#Storage)
and [`ol.clave.storage/TryLocker`](api/ol-clave-storage.adoc#TryLocker) using the local filesystem.
The presence of a lock file for a given name indicates a lock is held.
## Key-to-Path Mapping
Storage keys map directly to filesystem paths relative to a root directory.
Key normalization converts backslashes to forward slashes and strips leading
and trailing slashes.
Path traversal attempts (keys containing `..` that escape the root) throw
`IllegalArgumentException`.
## Atomic Writes
All writes use atomic move semantics: data is written to a temporary file
and renamed into place.
This ensures readers never see partial writes.
On filesystems that do not support atomic moves, a best-effort rename is used.
## Locking
Advisory locking is implemented with lock files in a `locks/` subdirectory.
Locks are created atomically by relying on the filesystem to enforce
exclusive file creation.
Processes that terminate abnormally will not have a chance to clean up their
lock files.
To handle this, while a lock is actively held, a background virtual thread
periodically updates a timestamp in the lock file (every 5 seconds).
If another process tries to acquire the lock but fails, it checks whether
the timestamp is still fresh.
If so, it waits by polling (every 1 second).
Otherwise, the stale lock file is deleted, effectively forcing an unlock.
While lock acquisition is atomic, unlocking is not perfectly atomic.
Filesystems offer atomic file creation but not necessarily atomic deletion.
It is theoretically possible for two processes to discover the same stale
lock and both attempt to delete it.
If one process deletes the lock file and creates a new one before the other
calls delete, the new lock may be deleted by mistake.
This means mutual exclusion is not guaranteed to be perfectly enforced in
the presence of stale locks.
However, these cases are rare, and we prefer the simpler solution over
alternatives that risk infinite loops.
## Filesystem Considerations
This implementation is designed for local filesystems and relies on specific
filesystem semantics:
* Exclusive file creation via `O_CREAT | O_EXCL`.
Lock acquisition depends on the filesystem atomically failing when creating
a file that already exists.
* Durable writes via `fsync`.
Lock file timestamps must survive crashes to enable stale lock detection.
Network filesystems (NFS, CIFS/SMB, AWS EFS) may not reliably support these
semantics.
In particular, some network filesystems do not honor `O_EXCL` across nodes or
do not guarantee that data is persisted after `fsync`, which can leave lock
files empty or corrupt after a crash or network interruption.
## Permissions
On POSIX systems, files are created with `rw-------` and directories with
`rwx------`.
On non-POSIX systems (Windows), default permissions apply.
## Usage
```clojure
(require '[ol.clave.storage.file :as fs]
'[ol.clave.storage :as s])
;; Use platform-appropriate directories for certificate storage
(def storage (fs/file-storage (fs/data-dir "myapp")))
;; Or specify a custom path
(def storage (fs/file-storage "/var/lib/myapp"))
;; Store and retrieve data
(s/store-string! storage nil "certs/example.com/cert.pem" cert)
(s/load-string storage nil "certs/example.com/cert.pem")
```
## Related Namespaces
* [`ol.clave.storage`](api/ol-clave-storage.adoc) - Storage protocol and utilities
* [`ol.clave.lease`](api/ol-clave-lease.adoc) - Cooperative cancellation
## ->LockMeta
```clojure
(->LockMeta created-ms updated-ms)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225)
---
## LockMeta
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225)
---
## map->LockMeta
```clojure
(map->LockMeta m)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L225-L225)
---
## ->FileStorage
```clojure
(->FileStorage root)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412)
---
## FileStorage
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412)
---
## map->FileStorage
```clojure
(map->FileStorage m)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L357-L412)
---
## home-dir
```clojure
(home-dir)
```
Returns the user’s home directory.
Uses the `user.home` system property, which the JVM resolves appropriately
for each platform.
Returns `nil` if the home directory cannot be determined.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L442-L449)
---
## state-dir
```clojure
(state-dir)
(state-dir app-name)
```
Returns the directory for persistent application state.
With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).
Checks environment variables in order:
1. `$STATE_DIRECTORY` - set by systemd for system units (already app-specific)
2. `$XDG_STATE_HOME` - XDG base directory spec / systemd user units
3. Platform default
When systemd sets `$STATE_DIRECTORY`, it may contain multiple colon-separated
paths if the unit configures multiple directories; this function returns the
first path.
Platform defaults when no environment variable is set:
| platform | path |
|----------|-------------------------------------|
| Linux | `$HOME/.local/state` |
| macOS | `$HOME/Library/Application Support` |
| Windows | `%LOCALAPPDATA%` |
Returns `nil` if a suitable directory cannot be determined.
```clojure
(state-dir) ; => "/home/user/.local/state"
(state-dir "myapp") ; => "/home/user/.local/state/myapp"
;; Under systemd system unit with StateDirectory=myapp:
(state-dir) ; => "/var/lib/myapp"
(state-dir "myapp") ; => "/var/lib/myapp" (no double append)
(file-storage (state-dir "myapp"))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L451-L500)
---
## config-dir
```clojure
(config-dir)
(config-dir app-name)
```
Returns the directory for application configuration files.
With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).
Checks environment variables in order:
1. `$CONFIGURATION_DIRECTORY` - set by systemd for system units (already app-specific)
2. `$XDG_CONFIG_HOME` - XDG base directory spec / systemd user units
3. Platform default
When systemd sets `$CONFIGURATION_DIRECTORY`, it may contain multiple
colon-separated paths if the unit configures multiple directories; this
function returns the first path.
Platform defaults when no environment variable is set:
| platform | path |
|----------|-----------------------------|
| Linux | `$HOME/.config` |
| macOS | `$HOME/Library/Preferences` |
| Windows | `%APPDATA%` |
Returns `nil` if a suitable directory cannot be determined.
```clojure
(config-dir) ; => "/home/user/.config"
(config-dir "myapp") ; => "/home/user/.config/myapp"
;; Under systemd system unit with ConfigurationDirectory=myapp:
(config-dir) ; => "/etc/myapp"
(config-dir "myapp") ; => "/etc/myapp" (no double append)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L502-L549)
---
## cache-dir
```clojure
(cache-dir)
(cache-dir app-name)
```
Returns the directory for application cache files.
With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).
Checks environment variables in order:
1. `$CACHE_DIRECTORY` - set by systemd for system units (already app-specific)
2. `$XDG_CACHE_HOME` - XDG base directory spec / systemd user units
3. Platform default
When systemd sets `$CACHE_DIRECTORY`, it may contain multiple colon-separated
paths if the unit configures multiple directories; this function returns the
first path.
Platform defaults when no environment variable is set:
| platform | path |
|----------|------------------------|
| Linux | `$HOME/.cache` |
| macOS | `$HOME/Library/Caches` |
| Windows | `%LOCALAPPDATA%` |
Returns `nil` if a suitable directory cannot be determined.
```clojure
(cache-dir) ; => "/home/user/.cache"
(cache-dir "myapp") ; => "/home/user/.cache/myapp"
;; Under systemd system unit with CacheDirectory=myapp:
(cache-dir) ; => "/var/cache/myapp"
(cache-dir "myapp") ; => "/var/cache/myapp" (no double append)
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L551-L598)
---
## data-dir
```clojure
(data-dir)
(data-dir app-name)
```
Returns the directory for persistent application data.
This is the recommended directory for ACME certificates and keys because they
are valuable cryptographic material with rate limits on reissuance.
With no arguments, returns the base directory (caller appends app name).
With `app-name`, appends it as a subdirectory (unless systemd provides one).
Checks environment variables in order:
1. `$STATE_DIRECTORY` - set by systemd (`StateDirectory=` maps to `/var/lib/` which is semantically correct)
2. `$XDG_DATA_HOME` - XDG base directory spec
3. Platform default
When systemd sets `$STATE_DIRECTORY`, it may contain multiple colon-separated
paths if the unit configures multiple directories; this function returns the
first path.
Platform defaults when no environment variable is set:
| platform | path |
|----------|-------------------------------------|
| Linux | `$HOME/.local/share` |
| macOS | `$HOME/Library/Application Support` |
| Windows | `%LOCALAPPDATA%` |
Returns `nil` if a suitable directory cannot be determined.
```clojure
(data-dir) ; => "/home/user/.local/share"
(data-dir "myapp") ; => "/home/user/.local/share/myapp"
;; Under systemd system unit with StateDirectory=myapp:
(data-dir) ; => "/var/lib/myapp"
(data-dir "myapp") ; => "/var/lib/myapp" (no double append)
(file-storage (data-dir "myapp"))
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L600-L652)
---
## file-storage
```clojure
(file-storage)
(file-storage root)
```
Creates a [`FileStorage`](#filestorage) rooted at `root.
`root` may be a string or `java.nio.file.Path`.
The directory is created if it does not exist.
Default: "ol.clave" subdir inside [`data-dir`](#data-dir)
Returns a record implementing [`ol.clave.storage/Storage`](api/ol-clave-storage.adoc#Storage) and [`ol.clave.storage/TryLocker`](api/ol-clave-storage.adoc#TryLocker).
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage/file.clj#L654-L671)
## ol.clave.storage
# ol.clave.storage
Key-value storage with path semantics for ACME certificate data.
The storage API provides a unified abstraction for persisting certificates,
account keys, and lock state.
Keys use forward slashes with no leading or trailing slashes.
A key with an associated value is a "file"; a key with no value that serves
as a prefix for other keys is a "directory".
Keys passed to [`load`](#load) and [`store!`](#store!) always have file semantics; directories
are implicit from the path structure.
## Key Format
Keys follow path semantics similar to filesystem paths.
A "prefix" is defined on a component basis: `"a"` is a prefix of `"a/b"`
but not of `"ab/c"`.
* Valid: `"acme/certs/example.com"`
* Valid: `"locks/renewal.lock"`
* Invalid: `"/leading/slash"` (leading slash removed by normalization)
* Invalid: `"trailing/slash/"` (trailing slash removed by normalization)
Use [`storage-key`](#storage-key) to safely join key components and [`safe-key`](#safe-key) to sanitize
user-provided values for use as key segments.
## Lease Integration
All operations accept a `lease` from [`ol.clave.lease`](api/ol-clave-lease.adoc) for cooperative
cancellation and deadline propagation.
Pass `nil` to skip cancellation checks for short, non-interruptible operations.
Implementations must honor lease cancellation and throw promptly when the
lease is no longer active.
## Locking
The [`lock!`](#lock!) and [`unlock!`](#unlock!) methods provide advisory locking to coordinate
expensive operations across processes.
You do not need to wrap every storage call in a lock; [`store!`](#store!), [`load`](#load),
and other basic operations are already thread-safe.
Use locking for higher-level operations that need synchronization, such as
certificate renewal where only one process should attempt issuance at a time.
When the lock guards an idempotent operation, always verify that the work
still needs to be done after acquiring the lock.
Another process may have completed the task while you were waiting.
Use [`with-lock`](#with-lock) for safe lock acquisition with guaranteed release.
Optional protocols [`TryLocker`](#trylocker) and [`LockLeaseRenewer`](#lockleaserenewer) extend locking
with non-blocking acquisition and lease renewal for long-running operations.
## Thread Safety
Implementations must be safe for concurrent use from multiple threads.
Methods should block until their operation is complete: [`load`](#load) should
always return the value from the last call to [`store!`](#store!) for a given key,
and concurrent calls to [`store!`](#store!) must not corrupt data.
Callers will typically invoke storage methods from virtual threads, so
blocking I/O is expected and appropriate.
Implementors do not need to spawn threads or perform asynchronous operations
internally.
This is not a streaming API and is not suitable for very large files.
## Usage
```clojure
(require '[ol.clave.storage :as s]
'[ol.clave.storage.file :as fs])
(let [storage (fs/file-storage "/var/acme")]
;; Store and retrieve data
(s/store-string! storage nil "certs/example.com" cert-pem)
(s/load-string storage nil "certs/example.com")
;; List with prefix
(s/list storage nil "certs" false) ; => ["certs/example.com" ...]
;; Coordinated access for expensive operations
(s/with-lock storage lease "certs/example.com"
(fn []
;; Check if work still needed after acquiring lock
(when (certificate-needs-renewal? storage "example.com")
(renew-certificate!)))))
```
## Related Namespaces
* [`ol.clave.storage.file`](api/ol-clave-storage-file.adoc) - Filesystem storage implementation
* [`ol.clave.lease`](api/ol-clave-lease.adoc) - Cooperative cancellation
This interface was inspired by certmagic’s interface in Go.
## ->KeyInfo
```clojure
(->KeyInfo key modified size terminal?)
```
Metadata about a storage key.
The `key` and `terminal?` fields are required.
The `modified` and `size` fields are optional if the storage implementation
cannot provide them, but setting them makes certain operations more consistent
and predictable.
Fields:
- `key` - the storage key as a string
- `modified` - last modification time as `java.time.Instant`, or `nil`
- `size` - size in bytes (Long), or `nil`
- `terminal?` - `false` for directories (keys that act as prefix for other
keys), `true` for files (keys with associated values)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L104-L119)
---
## KeyInfo
Metadata about a storage key.
The `key` and `terminal?` fields are required.
The `modified` and `size` fields are optional if the storage implementation
cannot provide them, but setting them makes certain operations more consistent
and predictable.
Fields:
- `key` - the storage key as a string
- `modified` - last modification time as `java.time.Instant`, or `nil`
- `size` - size in bytes (Long), or `nil`
- `terminal?` - `false` for directories (keys that act as prefix for other
keys), `true` for files (keys with associated values)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L104-L119)
---
## map->KeyInfo
```clojure
(map->KeyInfo m)
```
Metadata about a storage key.
The `key` and `terminal?` fields are required.
The `modified` and `size` fields are optional if the storage implementation
cannot provide them, but setting them makes certain operations more consistent
and predictable.
Fields:
- `key` - the storage key as a string
- `modified` - last modification time as `java.time.Instant`, or `nil`
- `size` - size in bytes (Long), or `nil`
- `terminal?` - `false` for directories (keys that act as prefix for other
keys), `true` for files (keys with associated values)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L104-L119)
---
## Storage
Key-value storage with path semantics.
All methods accept a `lease` from [`ol.clave.lease`](api/ol-clave-lease.adoc) for cooperative
cancellation; pass `nil` to skip cancellation checks.
Keys are normalized: backslashes become forward slashes, leading and trailing
slashes are stripped.
Implementations must be safe for concurrent use.
Methods should block until their operation is complete.
[`load`](#load), [`delete!`](#delete!), [`list`](#list), and [`stat`](#stat) should throw
`java.nio.file.NoSuchFileException` when the key does not exist.
_protocol_
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L121-L201)
### store!
```clojure
(store! this lease key value-bytes)
```
Stores `value-bytes` at `key`, creating parent directories as needed.
Overwrites any existing value at this key.
Concurrent calls to <> must not corrupt data.
Returns `nil`.
---
### load
```clojure
(load this lease key)
```
Returns the bytes stored at `key`.
Always returns the value from the last successful <> for this key.
Throws `java.nio.file.NoSuchFileException` if `key` does not exist.
---
### delete!
```clojure
(delete! this lease key)
```
Deletes `key` and any keys prefixed by it (recursive delete).
If `key` is a directory (prefix of other keys), all keys with that prefix
are deleted.
Returns `nil`.
Throws `java.nio.file.NoSuchFileException` if `key` does not exist.
---
### exists?
```clojure
(exists? this lease key)
```
Returns `true` if `key` exists as a file or directory, `false` otherwise.
---
### list
```clojure
(list this lease prefix recursive?)
```
Lists keys under `prefix`.
When `recursive?` is `false`, returns only keys prefixed exactly by
`prefix` (direct children).
When `recursive?` is `true`, non-terminal keys are enumerated and all
descendants are returned.
Returns a vector of key strings.
Throws `java.nio.file.NoSuchFileException` if `prefix` does not exist.
---
### stat
```clojure
(stat this lease key)
```
Returns a [`KeyInfo`](#keyinfo) record describing `key`.
Throws `java.nio.file.NoSuchFileException` if `key` does not exist.
---
### lock!
```clojure
(lock! this lease name)
```
Acquires an advisory lock for `name`, blocking until available.
Only one lock for a given name can exist at a time.
A call to <> for a name that is already locked blocks until the
lock is released or becomes stale.
Lock names are sanitized via <>.
Implementations must honor lease cancellation.
Returns `nil` when the lock is acquired.
---
### unlock!
```clojure
(unlock! this lease name)
```
Releases the advisory lock for `name`.
This method must only be called after a successful call to <>, and
only after the critical section is finished, even if it threw an exception.
<> cleans up any resources allocated during <>.
Returns `nil`.
Throws only if the lock could not be released.
---
## TryLocker
Optional non-blocking lock acquisition.
Implementations that support non-blocking lock attempts should extend this
protocol in addition to [`Storage`](#storage).
_protocol_
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L203-L218)
### try-lock!
```clojure
(try-lock! this lease name)
```
Attempts to acquire the lock for `name` without blocking.
Returns `true` if the lock was acquired, `false` if it could not be
obtained (e.g., already held by another process).
Implementations must honor lease cancellation.
After a successful <>, you must call <> when the
critical section is finished, even if it threw an exception.
---
## LockLeaseRenewer
Optional lease renewal for long-running locks.
When a lock is held for an extended period, the holder should periodically
renew the lock lease to prevent it from being considered stale and forcibly
released by another process.
This is useful for long-running operations that need synchronization.
_protocol_
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L220-L237)
### renew-lock-lease!
```clojure
(renew-lock-lease! this lease lock-key lease-duration)
```
Extends the lease on `lock-key` by `lease-duration`.
This prevents another process from acquiring the lock by treating it as
stale.
`lease-duration` is a `java.time.Duration`.
Returns `nil`.
Throws if the lock is not currently held or could not be renewed.
---
## safe-key
```clojure
(safe-key s)
```
Returns a filesystem-safe key component from `s`.
Transforms:
- Converts to lowercase
- Replaces spaces with underscores
- Replaces `+` with `_plus_`, `*` with `wildcard_`
- Replaces `:` with `-`
- Removes `..` sequences
- Strips characters not in `[a-zA-Z0-9_@.-]`
Use this when incorporating user input or domain names into storage keys.
```clojure
(safe-key "Example.COM") ; => "example.com"
(safe-key "*.example.com") ; => "wildcard_example.com"
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L241-L267)
---
## storage-key
```clojure
(storage-key & parts)
```
Joins key components with `/`, ignoring `nil` and blank parts.
```clojure
(storage-key "acme" "certs" "example.com")
;; => "acme/certs/example.com"
(storage-key "base" nil "" "file")
;; => "base/file"
```
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L269-L284)
---
## store-string!
```clojure
(store-string! storage lease key s)
```
Stores UTF-8 encoded `s` at `key`.
Convenience wrapper around [`store!`](#store!) for text content.
See [`load-string`](#load-string) for retrieval.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L286-L292)
---
## load-string
```clojure
(load-string storage lease key)
```
Loads UTF-8 text from `key`.
Convenience wrapper around [`load`](#load) for text content.
Throws `java.nio.file.NoSuchFileException` if `key` does not exist.
See [`store-string!`](#store-string!) for storage.
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L294-L301)
---
## with-lock
```clojure
(with-lock storage lease lock-name f)
```
Executes `f` while holding the lock `lock-name`, releasing on exit.
Acquires the lock via [`lock!`](#lock!), runs `f` (a zero-argument function), and
releases via [`unlock!`](#unlock!) in a finally block regardless of success or failure.
If the lock guards an idempotent operation, `f` should verify that the work
still needs to be done.
Another process may have completed the task while you were waiting to acquire
the lock.
```clojure
(with-lock storage lease "certs/example.com"
(fn []
;; Check if work still needed after acquiring lock
(when (certificate-needs-renewal? domain)
(renew-certificate!)
(write-certificate!))))
```
See also: [`lock!`](#lock!), [`unlock!`](#unlock!)
[source,window=_blank](https://github.com/outskirtslabs/clave/blob/main/src/ol/clave/storage.clj#L303-L329)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning).
## UNRELEASED
## `v0.0.1` (2026-XX-XX)
We haven’t quite got here yet..
Please report any problems and let me know if anything is unclear or inconvenient. Thank you.
## ol.clave
# ol.clave
> Automatic HTTPS certificate management and renewal via ACME, implemented in pure Clojure with minimal dependencies



[(https://img.shields.io/clojars/v/com.outskirtslabs/clave.svg)]
`ol.clave` is an [ACME client](https://datatracker.ietf.org/doc/html/rfc8555) for Clojure.
It gives you a lower-level API for implementing the ACME protocol itself when you need that much control. If you want to work directly with accounts, orders, authorizations, challenges, and certificate issuance, you can do that.
And if you do not want to live at RFC altitude all day, it also gives you a higher-level API for provisioning certificates from application code, managing them over time, and keeping them up to date. That is the part most people actually want. Ask for a cert. Renew the cert. Repeat.
`ol.clave` also includes adapters for wiring ACME into Jetty-based applications, plus helpers for HTTP-01 and TLS-ALPN-01 validation. For DNS validation, it gives you the hooks you need, but it does not ship DNS provider integrations by default. DNS provider integrations are future work. (If you are interested in that, [get in touch](https://casey.link/about).)
One of the design goals for `ol.clave` was as few dependencies as possible and avoid dragging in a bunch of Java ecosystem baggage.
At runtime there are exactly two hard dependencies. The first is [`babashka/json`](https://github.com/babashka/json), which is a BYO JSON-library-library. On the JVM it will use a JSON provider from your classpath. In many applications that will just work because you already have one. If you care which provider gets picked, or want to force one, go read that project’s docs.
The second runtime dep is [Peter Taoussanis’s Trove](https://github.com/taoensso/trove), which gives `ol.clave` a very lightweight way to emit signals and logging without forcing a backend on you. Same story there: wire it into whatever telemetry or logging stack you already use after reading Peter’s docs.
The no-dependencies choice has consequences. I did not want to pull in Bouncy Castle as a runtime dependency. It is large, it is heavy, and it is the kind of library that likes to start version fights when two things on your classpath want different releases. But ACME certificate provisioning still needs some plumbing that the JDK does not hand you directly (despite being distributed as part of the keytool util in every JDK package), especially around DER de/encoding, CSR generation, and a few X.509-adjacent details.
To be clear, `ol.clave` is not implementing any cryptography itself. The actual crypto primitives (RSA, ECC, etc) still come from the JVM. What `ol.clave` implements in pure Clojure is the narrow slice of encoding, decoding, and certificate-request machinery needed to make ACME work without the heavyweight runtime dependencies. That problem space is small enough to specify, small enough to test thoroughly and it isn’t a general-purpose PKI toolkit.
Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/clave {:mvn/version "0.0.0"}}}
;; Leiningen
[com.outskirtslabs/clave "0.0.0"]
```
## Quick Start
Start with one of the runnable examples in this repository:
* [`examples/certificate.clj`](https://github.com/outskirtslabs/clave/blob/main/examples/certificate.clj) for the higher-level certificate acquisition flow.
* [`examples/acme.clj`](https://github.com/outskirtslabs/clave/blob/main/examples/acme.clj) for a lower-level step-by-step ACME transaction.
* [`examples/ring_jetty.clj`](https://github.com/outskirtslabs/clave/blob/main/examples/ring_jetty.clj) for auto-renewing HTTPS with `ring-jetty-adapter`.
These examples use [Pebble](https://github.com/letsencrypt/pebble) for local ACME testing.
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.clave/next/)
* [API Reference](https://docs.outskirtslabs.com/ol.clave/next/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/clave/issues)
## Recommended Reading
* [RFC 8555: Automatic Certificate Management Environment (ACME)](https://datatracker.ietf.org/doc/html/rfc8555)
* [RFC 9773: ACME Renewal Information (ARI) Extension](https://datatracker.ietf.org/doc/html/rfc9773)
* [Best Practices for ACME Client Operations](https://github.com/https-dev/docs/blob/master/acme-ops.md)
## Security
See [Security](security.adoc) for security reporting and policy links.
## License
Copyright (C) 2025-2026 Casey Link
Distributed under the [EUPL-1.2](https://spdx.org/licenses/EUPL-1.2.html).
Some files included in this project are from third-party sources and retain their original licenses as indicated in [NOTICE](./NOTICE).
Special thanks to [Michiel Borkent (@borkdude)](https://github.com/borkdude/) for the use of [babashka/http-client](https://github.com/babashka/http-client) and [babashka/json](https://github.com/babashka/json).
## Security
# Security
Please report vulnerabilities through [GitHub Security Advisories](https://github.com/outskirtslabs/clave/security/advisories).
For general policy and support expectations, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
## ol.client-ip.cidr
# ol.client-ip.cidr
## cidr-parts
```clojure
(cidr-parts cidr)
```
Parse an ip network string into its prefix and signifcant bits
`cidr` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1)
Returns a vector of [^InetAddress prefix ^Integer bits]
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L26-L39)
---
## contains?
```clojure
(contains? cidr ip)
(contains? cidr-ip cidr-mask ip)
```
Check if the given CIDR contains the given IP address.
Arguments:
cidr - CIDR notation string (e.g. "192.168.0.0/24" or "2001:db8::/32")
ip - IP address to check (can be InetAddress or string)
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L41-L61)
---
## reserved-ranges
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L66-L93)
---
## reserved?
```clojure
(reserved? ip)
```
Check if an IP is in a reserved range (loopback or private network).
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/cidr.clj#L95-L98)
## ol.client-ip.core
# ol.client-ip.core
Core implementation of the client-ip library for extracting client IPs from forwarding headers.
The client-ip library is designed to accurately determine the original client IP address
from HTTP forwarding headers in Ring applications. It uses a strategy-based approach
where different strategies handle different network configurations.
## Main Features
* Strategy-based IP detection for different network setups
* Processes IPs according to the chosen strategy to prevent spoofing
* Supports all common headers: X-Forwarded-For, X-Real-IP, Forwarded (RFC 7239), etc.
## Usage
The main entry point is the `wrap-client-ip` middleware function that requires
a strategy and adds the `:ol/client-ip` key to the Ring request map:
```clojure
(require '[ol.client-ip.core :refer [wrap-client-ip]]
'[ol.client-ip.strategy :as strategy])
(def app
(-> handler
(wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")})
;; other middleware
))
```
You can also use `from-request` to extract client IPs directly from requests:
```clojure
(from-request request (strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2))
;; => "203.0.113.195"
```
See the strategy documentation for more details on choosing the right strategy.
## from-request
```clojure
(from-request request strategy)
```
Extract the client IP from a Ring request using the specified strategy.
This is the core function for determining client IPs. It takes a Ring request
and a strategy instance, then uses the strategy to determine the client IP
from the request headers and remote address.
Refer to ns docs and the [`wrap-client-ip`](#wrap-client-ip) docstring for more usage information.
Returns the client InetAddress , or nil if no client IP can be determined.
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/core.clj#L43-L58)
---
## wrap-client-ip
```clojure
(wrap-client-ip handler options)
```
Ring middleware that adds the client IP to the request map using strategies.
This middleware extracts the original client IP address using the specified
strategy and adds it to the request map as `:ol/client-ip`. The strategy determines
how headers are processed and which IP is considered the client IP.
## Options
The options map must contain:
* `:strategy` - A strategy instance (required)
## Strategy Selection
Choose the strategy that matches your network configuration:
* `RemoteAddrStrategy` - Direct connections (no reverse proxy)
* `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers
* `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs
* `RightmostTrustedCountStrategy` - Fixed number of trusted proxies
* `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges
* `ChainStrategy` - Try multiple strategies with fallback
## Examples
```clojure
;; Single reverse proxy with X-Real-IP
(def app
(-> handler
(wrap-client-ip {:strategy (strategy/single-ip-header-strategy "x-real-ip")})
;; other middleware
))
;; Multiple proxies with private IPs
(def app
(-> handler
(wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")})
;; other middleware
))
;; Chain strategy with fallback
(def app
(-> handler
(wrap-client-ip {:strategy
(strategy/chain-strategy
[(strategy/single-ip-header-strategy "x-real-ip")
(strategy/remote-addr-strategy)])})
;; other middleware
))
```
The middleware adds the `:ol/client-ip` key to the request map, containing the determined
client InetAddress or nil.
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/core.clj#L60-L121)
## ol.client-ip.ip
# ol.client-ip.ip
IP address utilities for conversion and classification.
This namespace provides functions for working with IP addresses,
particularly for converting between string, InetAddress, and numeric
representations. It follows RFC 5952 for IPv6 address formatting and
provides utilities for type checking and serialization.
**📌 NOTE**\
None of the functions in this namespace accept ip addreses input as strings.
Use [`ol.client-ip.parse-ip/from-string`](api/ol-client-ip-parse-ip.adoc#from-string) to convert strings to InetAddress objects.
The key operations available are:
* Type checking with `ipv4?` and `ipv6?`
* String formatting with `format-ip`
* Numeric conversion with `->numeric` and `numeric->`
These functions are designed to work with the `java.net.InetAddress`
class hierarchy and its subclasses `Inet4Address` and `Inet6Address`.
Refer to [`ol.client-ip.parse-ip`](api/ol-client-ip-parse-ip.adoc) and [`ol.client-ip.cidr`](api/ol-client-ip-cidr.adoc) for more useful functions.
## ipv4?
```clojure
(ipv4? ip)
```
Checks if an IP address is an IPv4 address.
This function determines if the given IP address is an instance of
`java.net.Inet4Address`.
Arguments:
ip - An IP address object (InetAddress)
Returns:
`true` if the IP address is IPv4, `false` otherwise
Examples:
```clojure
(ipv4? (parse-ip/from-string "192.168.1.1")) ;; => true
(ipv4? (parse-ip/from-string "::1")) ;; => false
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L28-L46)
---
## ipv6?
```clojure
(ipv6? ip)
```
Checks if an IP address is an IPv6 address.
This function determines if the given IP address is an instance of
`java.net.Inet6Address`.
Arguments:
ip - An IP address object (InetAddress)
Returns:
`true` if the IP address is IPv6, `false` otherwise
Examples:
```clojure
(ipv6? (parse-ip/from-string "2001:db8::1")) ;; => true
(ipv6? (parse-ip/from-string "192.168.1.1")) ;; => false
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L48-L66)
---
## format-ipv6
```clojure
(format-ipv6 ip)
```
Converts an IPv6 address to its canonical string representation.
This function formats IPv6 addresses according to RFC 5952, which includes:
- Using lowercase hexadecimal digits
- Compressing the longest run of consecutive zero fields with ::
- Not using :: to compress a single zero field
- Including scope IDs with % delimiter for link-local addresses
Arguments:
ip - An IPv6 address object (Inet6Address)
Returns:
A string representation of the IPv6 address formatted according to RFC 5952
Examples:
```clojure
(format-ipv6 (parse-ip/from-string "2001:db8:0:0:0:0:0:1"))
;; => "2001:db8::1"
(format-ipv6 (parse-ip/from-string "fe80::1%eth0"))
;; => "fe80::1%eth0"
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L121-L152)
---
## format-ip
```clojure
(format-ip ip)
```
Converts an IP address to its canonical string representation.
This function formats IP addresses according to standard conventions:
- IPv4 addresses use dotted decimal notation (e.g., "192.168.0.1")
- IPv6 addresses follow RFC 5952 with compressed notation using ::
for the longest run of zeros, and embedded IPv4 addresses where appropriate
The implementation handles scope IDs for IPv6 link-local addresses
and properly compresses IPv6 addresses according to the standard rules.
Arguments:
ip - An IP address object (InetAddress)
Returns:
A string representation of the IP address
Throws:
IllegalArgumentException - if the input is not a valid InetAddress
Examples:
```clojure
(format-ip (parse-ip/from-string "192.168.1.1")) ;; => "192.168.1.1"
(format-ip (parse-ip/from-string "2001:db8::1")) ;; => "2001:db8::1"
(format-ip (parse-ip/from-string "::ffff:1.2.3.4")) ;; => "::ffff:1.2.3.4"
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L154-L185)
---
## ->numeric
```clojure
(->numeric ip)
```
Converts an IP address to its numeric representation as a BigInteger.
This function takes an InetAddress (either IPv4 or IPv6) and returns
a BigInteger representing the address. The BigInteger can be used for
IP address arithmetic, comparison, or storage.
Arguments:
ip - An IP address object (InetAddress)
Returns:
A BigInteger representing the IP address
Examples:
```clojure
(->numeric (parse-ip/from-string "192.0.2.1"))
;; => 3221225985
(->numeric (parse-ip/from-string "2001:db8::1"))
;; => 42540766411282592856903984951653826561
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L187-L209)
---
## numeric->
```clojure
(numeric-> address version)
```
Converts a BigInteger to an InetAddress.
This function takes a numeric representation of an IP address as a BigInteger
and converts it back to an InetAddress object. The `ipv6?` flag determines
whether to create an IPv4 or IPv6 address.
Arguments:
address - A BigInteger representing the IP address
version - One of :v6 or :v4 indicating whether this is an IPv6 address
or an IPv4 address
Returns:
An InetAddress object representing the numeric address, or nil
Throws:
ExceptionInfo - If the BigInteger is negative or too large for the
specified address type
Examples:
```clojure
;; Convert back to IPv4
(numeric-> (BigInteger. "3221226113") :v4)
;; => #object[java.net.Inet4Address 0x578d1c5 "/192.0.2.129"]
;; Convert back to IPv6
(numeric-> (BigInteger. "42540766411282592856903984951653826561") :v6)
;; => #object[java.net.Inet6Address 0x14832c23 "/2001:db8:0:0:0:0:0:1"]
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L211-L267)
---
## numeric-v6->
```clojure
(numeric-v6-> address)
```
Converts a BigInteger to an IPv6 address.
This is a convenience wrapper around `numeric->` that automatically
sets the `ipv6?` flag to true.
Arguments:
address - A BigInteger representing the IPv6 address
Returns:
An Inet6Address object or nil if the input is invalid
See also: <<numeric--GT-,`numeric->`>>
Examples:
```clojure
(numeric-v6-> (BigInteger. "42540766411282592856903984951653826561"))
;; => #object[java.net.Inet6Address 0x42668812 "/2001:db8:0:0:0:0:0:1"]
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L269-L292)
---
## numeric-v4->
```clojure
(numeric-v4-> address)
```
Converts a BigInteger to an IPv4 address.
Arguments:
address - A BigInteger representing the IPv4 address
Returns:
An Inet4Address object or nil if the input is invalid
See also: <<numeric--GT-,`numeric->`>>
Examples:
```clojure
(numeric-v4-> (BigInteger. "3221226113"))
;; => #object[java.net.Inet4Address 0x6b88aeff "/192.0.2.129"]
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/ip.clj#L294-L314)
## ol.client-ip.parse-ip
# ol.client-ip.parse-ip
Parse a string representation of an IPv4 or IPv6 address into an InetAddress instance.
Unlike `InetAddress/getByName`, the functions in this namespace never cause
DNS services to be accessed. This avoids potentially blocking/side-effecting
network calls that can occur when using the JDK’s built-in methods to parse IP
addresses.
This implementation is inspired by Google Guava’s InetAddresses class, which
provides similar functionality in Java. This code focuses on strict validation
of IP address formats according to relevant RFCs.
Features:
- Non-blocking IP address parsing with pure functions
- Strict RFC-compliant validation
- Support for all IPv4 formats
- Support for all IPv6 formats (including compressed notation and embedded IPv4)
- Support for IPv6 scope identifiers
The main entry point is `parse-ip`, which takes a string and returns an
InetAddress instance or nil if the input is an invalid ip address literal.
## bytes->inet-address
```clojure
(bytes->inet-address addr)
(bytes->inet-address addr scope)
```
Convert a byte array into an InetAddress.
Args:
addr: the raw 4-byte or 16-byte IP address in big-endian order
scope: optional scope identifier for IPv6 addresses
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/parse_ip.clj#L231-L256)
---
## from-string
```clojure
(from-string ip-string)
```
Returns the InetAddress having the given string representation or nil otherwise.
This function parses IP address strings without performing DNS lookups, making it
suitable for environments where DNS lookups would cause unwanted blocking or side effects.
It supports:
- IPv4 addresses in dotted decimal format (e.g., "192.168.1.1")
- IPv6 addresses in hex format with optional compression (e.g., "2001:db8::1")
- IPv6 addresses with scope IDs (e.g., "fe80::1%eth0" or "fe80::1%1")
- IPv6 addresses with embedded IPv4 (e.g., "::ffff:192.168.1.1")
If the input is already an InetAddress, it is returned unchanged.
Args:
ip-string: A string representing an IP address or an InetAddress
Returns:
An InetAddress object, or nil if the input couldn’t be parsed
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/parse_ip.clj#L258-L290)
## ol.client-ip.protocols
# ol.client-ip.protocols
## ClientIPStrategy
Protocol for determining client IP from headers and remote address.
Returns InetAddress or nil if no valid client ip is found.
Implementations should:
* Be thread-safe and reusable across requests
* Validate configuration at creation time (throw on invalid config)
_protocol_
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/protocols.clj#L5-L21)
### client-ip
```clojure
(client-ip this headers remote-addr)
```
Extract the client IP from request headers and remote address.
Args:
headers: Ring-style headers map (lowercase keys)
remote-addr: String from Ring request :remote-addr
Returns:
InetAddress or nil if no valid client IP found.
## ol.client-ip.strategy
# ol.client-ip.strategy
Strategy implementations for determining client IP addresses.
This namespace provides various strategy implementations for extracting the
real client IP from HTTP headers and connection information.
Each strategy is designed for specific network configurations:
* `RemoteAddrStrategy` - Direct connections (no reverse proxy)
* `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers
* `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs
* `LeftmostNonPrivateStrategy` - Get IP closest to original client (not secure)
* `RightmostTrustedCountStrategy` - Fixed number of trusted proxies
* `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges
* `ChainStrategy` - Try multiple strategies in order
Strategies are created once and reused across requests. They are thread-safe
and designed to fail fast on configuration errors while being resilient to
malformed input during request processing.
## parse-forwarded
```clojure
(parse-forwarded header-value)
```
Parse a Forwarded header value according to RFC 7239.
Returns a sequence of InetAddress objects, or an empty sequence if none are valid.
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L119-L130)
---
## split-host-zone
```clojure
(split-host-zone ip-string)
```
Split IPv6 zone from IP address.
IPv6 addresses can include zone identifiers (scope IDs) separated by '%'.
For example: 'fe80::1%eth0' or 'fe80::1%1'
Args:
ip-string: String IP address that may contain a zone identifier
Returns:
Vector of [ip zone] where zone is the zone identifier string,
or [ip nil] if no zone is present.
Examples:
(split-host-zone "192.168.1.1") => ["192.168.1.1" nil]
(split-host-zone "fe80::1%eth0") => ["fe80::1" "eth0"]
(split-host-zone "fe80::1%1") => ["fe80::1" "1"]
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L150-L171)
---
## remote-addr-strategy
```clojure
(remote-addr-strategy)
```
Create a strategy that uses the direct client socket IP.
This strategy extracts the IP address from the request’s :remote-addr field,
which represents the direct client socket connection. Use this when your
server accepts direct connections from clients (not behind a reverse proxy).
The strategy strips any port information and validates the IP is not
zero/unspecified (0.0.0.0 or ::).
Returns:
RemoteAddrStrategy instance
Example:
(remote-addr-strategy)
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L184-L200)
---
## single-ip-header-strategy
```clojure
(single-ip-header-strategy header-name)
```
Create a strategy that extracts IP from headers containing a single IP address.
This strategy is designed for headers like X-Real-IP, CF-Connecting-IP,
True-Client-IP, etc. that contain only one IP address. Use this when you
have a single trusted reverse proxy that sets a reliable single-IP header.
The strategy validates that the header name is not X-Forwarded-For or
Forwarded, as these headers can contain multiple IPs and should use
different strategies.
Args:
header-name: String name of the header to check (case-insensitive)
Returns:
SingleIPHeaderStrategy instance
Throws:
ExceptionInfo if header-name is invalid or refers to multi-IP headers
Examples:
(single-ip-header-strategy "x-real-ip")
(single-ip-header-strategy "cf-connecting-ip")
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L208-L235)
---
## rightmost-non-private-strategy
```clojure
(rightmost-non-private-strategy header-name)
```
Create a strategy that gets the rightmost non-private IP from forwarding headers.
This strategy processes IPs in reverse order (rightmost first) and returns
the first IP that is not in a private/reserved range. Use this when all
your reverse proxies have private IP addresses, so the rightmost non-private
IP should be the real client IP.
The strategy supports X-Forwarded-For and Forwarded headers that contain
comma-separated IP lists.
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
Returns:
RightmostNonPrivateStrategy instance
Throws:
ExceptionInfo if header-name is invalid or refers to single-IP headers
Examples:
(rightmost-non-private-strategy "x-forwarded-for")
(rightmost-non-private-strategy "forwarded")
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L250-L278)
---
## leftmost-non-private-strategy
```clojure
(leftmost-non-private-strategy header-name)
```
Create a strategy that gets the leftmost non-private IP from forwarding headers.
This strategy processes IPs in forward order (leftmost first) and returns
the first IP that is not in a private/reserved range. This gets the IP
closest to the original client.
⚠️ ***WARNING: NOT FOR SECURITY USE*** ⚠️
This strategy is easily spoofable since clients can add arbitrary IPs to
the beginning of forwarding headers. Use only when security is not a concern
and you want the IP closest to the original client.
The strategy supports X-Forwarded-For and Forwarded headers that contain
comma-separated IP lists.
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
Returns:
LeftmostNonPrivateStrategy instance
Throws:
ExceptionInfo if header-name is invalid or refers to single-IP headers
Examples:
(leftmost-non-private-strategy "x-forwarded-for")
(leftmost-non-private-strategy "forwarded")
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L293-L325)
---
## rightmost-trusted-count-strategy
```clojure
(rightmost-trusted-count-strategy header-name trusted-count)
```
Create a strategy that returns IP at specific position based on known proxy count.
This strategy is for when you have a fixed number of trusted proxies and want
to get the IP at the specific position that represents the original client.
Given N trusted proxies, the client IP should be at position -(N+1) from the end.
For example, with 2 trusted proxies and header 'client, proxy1, proxy2, proxy3':
- Total IPs: 4
- Skip last 2 (trusted): positions 2,3
- Return IP at position 1 (proxy1)
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
trusted-count: Number of trusted proxies (must be > 0)
Returns:
RightmostTrustedCountStrategy instance
Throws:
ExceptionInfo if header-name is invalid or trusted-count <= 0
Examples:
(rightmost-trusted-count-strategy "x-forwarded-for" 1)
(rightmost-trusted-count-strategy "forwarded" 2)
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L338-L371)
---
## rightmost-trusted-range-strategy
```clojure
(rightmost-trusted-range-strategy header-name trusted-ranges)
```
Create a strategy that returns rightmost IP not in trusted ranges.
This strategy processes IPs in reverse order (rightmost first) and returns
the first IP that is not within any of the specified trusted IP ranges.
Use this when you know the specific IP ranges of your trusted proxies.
The strategy supports X-Forwarded-For and Forwarded headers that contain
comma-separated IP lists.
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
trusted-ranges: Collection of CIDR ranges (strings) that represent trusted proxies
Each range should be a valid CIDR notation like "192.168.1.0/24"
Returns:
RightmostTrustedRangeStrategy instance
Throws:
ExceptionInfo if header-name is invalid or trusted-ranges is empty/invalid
Examples:
(rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8" "192.168.0.0/16"])
(rightmost-trusted-range-strategy "forwarded" ["172.16.0.0/12"])
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L386-L424)
---
## chain-strategy
```clojure
(chain-strategy strategies)
```
Create a strategy that tries multiple strategies in order until one succeeds.
This strategy allows fallback scenarios where you want to try different
IP detection methods in priority order. Each strategy is tried in sequence
until one returns a non-empty result.
Common use case: Try a single-IP header first, then fall back to remote address.
Args:
strategies: Vector of strategy instances to try in order
Must contain at least one strategy
Returns:
ChainStrategy instance
Throws:
ExceptionInfo if strategies is empty or contains invalid strategies
Examples:
(chain-strategy [(single-ip-header-strategy "x-real-ip")
(remote-addr-strategy)])
(chain-strategy [(rightmost-non-private-strategy "x-forwarded-for")
(single-ip-header-strategy "x-real-ip")
(remote-addr-strategy)])
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/v0.1.x/src/ol/client_ip/strategy.clj#L435-L470)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## ++[++UNRELEASED++]++
## `v0.1.1` (2025-08-18)
Sorry, this is my first from-scratch clojars release, and I’m struggling
to get the metadata working.
There are no changes.
## `v0.1.0` (2025-08-18)
This is the first public release of this codebase under the
`ol.client-ip` moniker.
Variations of this code has been used in production for a long time, but
nonetheless I want to be cautious so we’re starting with sub-1.0
versioning.
Please report any problems and let me know if anything is unclear,
inconvenient, etc. Thank you! 🙏
## ol.client-ip
# ol.client-ip
> A 0-dependency ring middleware for determining a request’s real client IP address from HTTP headers




`X-Forwarded-For` and other client IP headers are [often used incorrectly](https://adam-p.ca/blog/2022/03/x-forwarded-for/), resulting in bugs and security vulnerabilities. This library provides strategies for extracting the correct client IP based on your network configuration.
It is based on the golang reference implementation [realclientip/realclientip-go](https://github.com/realclientip/realclientip-go).
Quick feature list:
* ring middleware determining the client’s IP address
* 0 dependency IP address string parsing with guaranteed no trips to the hosts' DNS services, which can block and timeout (unlike Java’s `InetAddress/getByName`)
* rightmost-ish strategies support the `X-Forwarded-For` and `Forwarded` (RFC 7239) headers
* IPv6 zone identifiers support
Note that there is no dependency on ring, the public API could also be used for pedestal or sieppari-style interceptors.
Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/client-ip {:mvn/version "0.1.1"}}}
;; Leiningen
[com.outskirtslabs/client-ip "0.1.1"]
```
## Quick Start
```clojure
(ns myapp.core
(:require [ol.client-ip.core :as client-ip]
[ol.client-ip.strategy :as strategy]))
;; Simple case: behind a trusted proxy that sets X-Real-IP
(def app
(-> handler
(client-ip/wrap-client-ip
{:strategy (strategy/single-ip-header-strategy "x-real-ip")})))
;; The client IP is now available in the request
(defn handler [request]
(let [client-ip (:ol/client-ip request)]
{:status 200
:body (str "Your IP is: " client-ip)}))
```
For detailed guidance on choosing strategies, see [Usage Guide](usage.adoc).
Choosing the wrong strategy can result in IP address spoofing security vulnerabilities.
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.client-ip/0.1/)
* [API Reference](https://docs.outskirtslabs.com/ol.client-ip/0.1/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/client-ip/issues)
## Recommended Reading
You think it is an easy question:
> I have an HTTP application, I just want to know the IP address of my client.
But who is the client?
> The computer on the other end of the network connection?
But which network connection? The one connected to your HTTP application is probably a reverse proxy or load balancer.
> Well I mean the "user’s IP address"
It ain’t so easy kid.
There are many good articles on the internet that discuss the perils and pitfalls of trying to answer this deceptively simple question.
You _should_ read one or two of them to get an idea of the complexity in this space. Libraries, like this one, _cannot_ hide the complexity from you, there is no abstraction nor encapsulation nor "default best practice".
Below are some of those good articles:
* MDN: [X-Forwarded-For: Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#security_and_privacy_concerns)
* [The perils of the "real" client IP](https://adam-p.ca/blog/2022/03/x-forwarded-for/) ([archive link](https://web.archive.org/web/20250416042714/https://adam-p.ca/blog/2022/03/x-forwarded-for/))
* [Rails IP Spoofing Vulnerabilities and Protection](https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) ([archive link](https://web.archive.org/web/20250421121810/https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/))
* [ol.client-ip: A Clojure Library to Prevent IP Spoofing](https://casey.link/blog/client-ip-ring-middleware/)
## Security
See [here](https://github.com/outskirtslabs/client-ip/security/advisories) for security advisories or to report a security vulnerability.
## License
Copyright (C) 2025 Casey Link <casey@outskirtslabs.com>
Distributed under the [MIT License](https://github.com/outskirtslabs/client-ip/blob/main/LICENSE).
## Security policy
# Security policy
## Advisories
All security advisories will be posted
https://github.com/outskirtslabs/client-ip/security/advisories[on
GitHub].
## Reporting a vulnerability
Please report possible security vulnerabilities
https://github.com/outskirtslabs/client-ip/security/advisories[via
GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt
emails with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see
[Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
— [Casey Link](https://casey.link)
## Usage Guide
# Usage Guide
This guide explains when and why to use each strategy. The choice of strategy is
critical -- _using the wrong strategy can lead to IP spoofing vulnerabilities_.
## Understanding Your Network Setup
Before choosing a strategy, you need to understand your production environment’s network topology:
1. Are you behind reverse proxies? (nginx, Apache, load balancers, CDNs)
2. How many trusted proxies are there?
3. What headers do they set?
The answers determine which strategy is appropriate and secure for your setup.
## Terminology
* **socket-level IP**\
The IP address that a server or proxy observes at the TCP socket level from whatever is directly connecting to it. This is always trustworthy since it cannot be spoofed at the socket level, regardless of whether the connection is from an end user or another proxy in the chain.
* **private IP addresses**\
IP addresses reserved for private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) that are not routable on the public internet. These typically represent your internal infrastructure components.
* **reverse proxy / proxy / load balancer / CDN**\
An intermediary server that sits between clients and your application server, forwarding client requests. Examples include: nginx, Apache, caddy, AWS ALB, Cloudflare, Cloudfront, Fastly, Akamai, Fly.io, Hetzner Load Balancer.
* **trusted proxy**\
A reverse proxy under your control or operated by a service you trust, whose IP forwarding headers you can rely on. The key characteristic is that you know what headers it sets and clients cannot bypass it.
* **IP spoofing**\
The practice of sending packets with a forged source IP address, or in the context of HTTP headers, setting fake client IP values in headers like `X-Forwarded-For` to deceive the application about the request’s true origin.
* **trivially spoofable**\
Headers or IP values that can be easily faked by any client without special network access. For example, any client can send `X-Forwarded-For: 1.2.3.4` in their request.
* **XFF**\
Short for `X-Forwarded-For`, the most common HTTP header used by proxies to communicate the original client IP address through a chain of proxies. Each proxy typically appends the IP it received the request from.
* **rightmost-ish**\
A parsing strategy that extracts IP addresses from the right side of forwarding headers like `X-Forwarded-For`, typically skipping a known number of trusted proxies to find the IP added by the first trusted proxy. This is the only trustworthy approach since you control what your trusted proxies add to the header chain ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)).
* **leftmost-ish**\
A parsing strategy that takes the leftmost (first) IP address in forwarding headers, representing the alleged original client. This is highly untrustworthy and vulnerable to spoofing since any client can set arbitrary values at the beginning of these headers ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)).
## Strategy Reference
### Remote Address Strategy / No middleware at all!
* **When to use**\
Your server accepts direct connections from clients without any reverse proxies or load balancers.
* **Why**\
The socket-level remote address is the actual client IP when there are no intermediaries.
Using this library you can use the remote-addr-strategy, which is provided to be comprehensive, however you can also just use the existing [`:remote-addr`](https://github.com/ring-clojure/ring/blob/master/SPEC.md#remote-addr) key in your ring request map.
```clojure
(strategy/remote-addr-strategy)
;; OR, just fetch the remote addr from the ring request map
(:remote-addr request)
```
* **Example network**
```
Client -> Your Server
```
* **Security**\
Safe -- the remote address cannot be spoofed at the socket level.
---
### Single IP Header Strategy
* **When to use**\
You have a trusted reverse proxy that accepts connections directly from clients and sets a specific header with the client IP.
* **Why**\
Many Cloud Provider’s CDNs and load balancers products provide a single authoritative header with the real client IP. This is the most secure approach when you have such a setup.
* **Provider headers with trusted values**
* Cloudflare (everyone): `cf-connecting-ip` -- this is a socket-level IP value ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/))
* Cloudflare (enterprise): `true-client-ip` -- also a socket-level IP value, and just for Enterprise customers with backwards compatibility requirements ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/))
* Fly.io: `fly-client-ip` -- the socket ip address ([docs](https://www.fly.io/docs/networking/request-headers/#fly-client-ip))
* Azure FrontDoor: `x-azure-socketip` -- the socket IP address associated with the TCP connection that the current request originated from ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol))
* nginx with correctly configured [`ngx_http_realip_module`](https://nginx.org/en/docs/http/ngx_http_realip_module.html)
* Do not use `proxy_set_header X-Real-IP $remote_addr;`
* **Provider headers that require extra config**
These providers offer headers that out of the box are trivially spoofable, and require extra configuration (in your provider’s management interface) to configure securely.
* Akamai: `true-client-ip` -- trivially spoofable by default, refer to [this writeup](https://adam-p.ca/blog/2022/03/x-forwarded-for/#akamai)
* Fastly: `fastly-client-ip` -- trivially spoofable, you must use vcl to configure it [fastly docs](https://www.fastly.com/documentation/reference/http/http-headers/Fastly-Client-IP/)
* **Provider headers to avoid**
**⚠️ WARNING**\
In nearly all of these cases you are better off using XFF and reasoning about the number of proxies or their network addresses.
* Azure FrontDoor: `x-azure-clientip` -- trivially spoofable as it is the leftmost-ish XFF ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol))
```clojure
;; Clients connect to your application server *only* through and *directly* through Cloudflare
(strategy/single-ip-header-strategy "cf-connecting-ip")
;; Clients connect to your application server *only* through and *directly* through Fly.io
(strategy/single-ip-header-strategy "fly-client-ip")
```
* **Example networks**
```
Client -> Cloudflare -> Your Server
Client -> nginx -> Your Server
Client -> Load Balancer -> Your Server
```
* **Security considerations**
* Ensure the header cannot be spoofed by clients
* Verify clients cannot bypass your proxy
* Use only the header _your_ trusted proxy sets
**⚠️ WARNING**\
Common mistakes
* Using `x-forwarded-for` with this strategy (use rightmost strategies instead)
* Not validating that clients must go through your proxy
* Changes to your topology that break a rule that was previously true
* For example, adding another proxy in the chain or exposing servers on a different interface
* Using headers that can be set by untrusted sources
---
### Rightmost Non-Private Strategy
* **When to use**\
You have multiple reverse proxies between the internet and the server, all using private IP addresses, and they append to `X-Forwarded-For` or `Forwarded` headers.
* **Why**\
In a typical private network setup, the rightmost non-private IP in the trusted forwarding chain is the real client IP. Private IPs represent your infrastructure.
```clojure
(strategy/rightmost-non-private-strategy "x-forwarded-for")
;; Using RFC 7239 Forwarded header
(strategy/rightmost-non-private-strategy "forwarded")
```
* **Example network**
```
Client -> Internet Proxy -> Private Load Balancer -> Private App Server
(1.2.3.4) (10.0.1.1) (10.0.2.1)
X-Forwarded-For: 1.2.3.4, 10.0.1.1
Result: 1.2.3.4 (rightmost non-private)
```
* **Security**\
Secure. Attackers can still spoof the leftmost entries, but not the rightmost non-private IP.
**⚠️ WARNING**\
Common Mistakes
* Do not use when your proxies have public IP addresses
* Do not use when you need to trust specific proxy IPs (use trusted range strategy instead)
---
### Rightmost Trusted Count Strategy
* **When to use**\
You know exactly how many trusted proxies append IPs to the header, and you want the IP added by the first trusted proxy.
* **Why**\
When you have a fixed, known number of trusted proxies, counting backwards gives you the IP that was added by your first trusted proxy (the client IP it saw).
```clojure
;; Two trusted proxies
(strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2)
;; One trusted proxy
(strategy/rightmost-trusted-count-strategy "forwarded" 1)
```
* **Example with count=2**
```
Client -> Proxy1 -> Proxy2 -> Your Server
(adds A) (adds B)
X-Forwarded-For: A, B
With count=2: Skip 2 from right, return A
```
* **Security**\
Secure, when your proxy count is stable and known.
**⚠️ WARNING**\
Common Mistakes
* Count must exactly match your trusted proxy count
* If the count is wrong, you’ll get incorrect results or errors
* Network topology changes require updating the count on the application server
---
### Rightmost Trusted Range Strategy
* **When to use**\
You know the IP ranges of all your trusted proxies and want the rightmost IP that’s not from a trusted source.
* **Why**\
This is the most flexible strategy for complex infrastructures where you know your proxy IPs but they might change within known ranges.
```clojure
;; You have a VPC where your client-facing load balancer could be any ip address
;; inside the 10.1.1.0/24 subnet (az1) or 10.1.2.0/24 (az2)
(strategy/rightmost-trusted-range-strategy
"x-forwarded-for"
["10.1.1.0/24" "10.1.2.0/24"])
;; Including Cloudflare ranges (these are examples, do not copy them!)
(strategy/rightmost-trusted-range-strategy
"x-forwarded-for"
["173.245.48.0/20" "103.21.244.0/22" ; Example Cloudflare IPv4 ranges
"2400:cb00::/32"]) ; Example Cloudflare IPv6 range
```
* **Example**
```
Client -> Cloudflare -> Your Load Balancer -> App Server
1.2.3.4 173.245.48.1 10.0.1.1
X-Forwarded-For: 1.2.3.4, 173.245.48.1, 10.0.1.1
Trusted ranges: ["173.245.48.0/20", "10.0.0.0/8"]
Result: 1.2.3.4 (rightmost IP not in trusted ranges)
```
* **Security**\
Secure, when ranges are properly maintained.
**⚠️ WARNING**\
Common Mistakes
* Forgetting to keep the trusted ranges up to date
* Not including all possible proxy IPs
* When using a cloud provider, not using their API for current up to date ranges
---
### Chain Strategy
* **When to use**\
You have multiple possible network paths to your server and need fallback behavior.
**⚠️ WARNING**\
Do not abuse ChainStrategy to check multiple headers. There is likely only one header you should be checking, and checking more can leave you vulnerable to IP spoofing. Each strategy should represent a different network path, not multiple ways to parse the same path.
* **Why**\
Real-world deployments can have multiple possible configurations (direct connections or via CDN). Chain strategy tries each approach until one succeeds.
```clojure
;; Clients can connect via cloudflare or directly to your server
(strategy/chain-strategy
[(strategy/single-ip-header-strategy "cf-connecting-ip")
(strategy/remote-addr-strategy)])
;; Multiple fallback levels
(strategy/chain-strategy
[(strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8"])
(strategy/rightmost-non-private-strategy "x-forwarded-for")
(strategy/remote-addr-strategy)])
```
* **Example use cases**
* Development vs production environments
* Gradual migration between proxy setups
* Handling both CDN and direct traffic
---
### Leftmost Non-Private Strategy
**⚠️ WARNING**\
DO NOT USE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING. The leftmost IP can be trivially spoofed by clients.
* **When to use**\
Rarely recommended. Only when you specifically need the IP allegedly closest to the original client (knowing full well that it can be spoofed).
* **Why**\
This gives you the "apparent" client IP but offers _no security_ against spoofing.
```clojure
(strategy/leftmost-non-private-strategy "x-forwarded-for")
(strategy/leftmost-non-private-strategy "forwarded")
```
* **Example**
```
X-Forwarded-For: 1.2.3.4, 2.3.4.5, 192.168.1.1
Result: 1.2.3.4 (leftmost non-private)
```
* **Valid use cases**
* Debugging or logging where you want the "claimed" client IP
* Analytics where approximate location matters more than accuracy
## Testing Your Configuration
Before deploying features that rely on the client IP address, deploy this
middleware with logging into your production network and verify your strategy
works correctly:
1. Test with expected traffic: Ensure you get the right IP for normal requests
2. Test spoofing attempts: Verify that fake headers are ignored, you can spoof headers easily with `curl -H "X-Forwarded-For: 1.2.3.4" ...`
3. Monitor for empty results: The middleware returns `nil` when a failure occurs, this could indicate an attack or a configuration problem.
## Common Pitfalls
1. Using multiple headers: Never chain strategies that check different headers from the same request
2. Wrong header choice: For example, using `x-forwarded-for` when your proxy sets `x-real-ip`
3. Ignoring network changes: Proxy counts or ranges change but your app configuration doesn’t
4. Development vs production: Different strategies needed for different environments
5. Not validating proxy control: Assuming headers can’t be spoofed when they can, don’t just trust the hyperscalers or commercial CDNs, _verify_.
Remember: _the right strategy depends entirely on your specific network configuration_. When in doubt, analyze real traffic in a real network setting.
## ol.client-ip.cidr
# ol.client-ip.cidr
## cidr-parts
```clojure
(cidr-parts cidr)
```
Parse an ip network string into its prefix and signifcant bits
`cidr` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1)
Returns a vector of [^InetAddress prefix ^Integer bits]
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L26-L39)
---
## contains?
```clojure
(contains? cidr ip)
(contains? cidr-ip cidr-mask ip)
```
Check if the given CIDR contains the given IP address.
Arguments:
cidr - CIDR notation string (e.g. "192.168.0.0/24" or "2001:db8::/32")
ip - IP address to check (can be InetAddress or string)
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L41-L61)
---
## reserved-ranges
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L66-L93)
---
## reserved?
```clojure
(reserved? ip)
```
Check if an IP is in a reserved range (loopback or private network).
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/cidr.clj#L95-L98)
## ol.client-ip.core
# ol.client-ip.core
Core implementation of the client-ip library for extracting client IPs from forwarding headers.
The client-ip library is designed to accurately determine the original client IP address
from HTTP forwarding headers in Ring applications. It uses a strategy-based approach
where different strategies handle different network configurations.
## Main Features
* Strategy-based IP detection for different network setups
* Processes IPs according to the chosen strategy to prevent spoofing
* Supports all common headers: X-Forwarded-For, X-Real-IP, Forwarded (RFC 7239), etc.
## Usage
The main entry point is the `wrap-client-ip` middleware function that requires
a strategy and adds the `:ol/client-ip` key to the Ring request map:
```clojure
(require '[ol.client-ip.core :refer [wrap-client-ip]]
'[ol.client-ip.strategy :as strategy])
(def app
(-> handler
(wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")})
;; other middleware
))
```
You can also use `from-request` to extract client IPs directly from requests:
```clojure
(from-request request (strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2))
;; => "203.0.113.195"
```
See the strategy documentation for more details on choosing the right strategy.
## from-request
```clojure
(from-request request strategy)
```
Extract the client IP from a Ring request using the specified strategy.
This is the core function for determining client IPs. It takes a Ring request
and a strategy instance, then uses the strategy to determine the client IP
from the request headers and remote address.
Refer to ns docs and the [`wrap-client-ip`](#wrap-client-ip) docstring for more usage information.
Returns the client InetAddress , or nil if no client IP can be determined.
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/core.clj#L43-L58)
---
## wrap-client-ip
```clojure
(wrap-client-ip handler options)
```
Ring middleware that adds the client IP to the request map using strategies.
This middleware extracts the original client IP address using the specified
strategy and adds it to the request map as `:ol/client-ip`. The strategy determines
how headers are processed and which IP is considered the client IP.
## Options
The options map must contain:
* `:strategy` - A strategy instance (required)
## Strategy Selection
Choose the strategy that matches your network configuration:
* `RemoteAddrStrategy` - Direct connections (no reverse proxy)
* `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers
* `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs
* `RightmostTrustedCountStrategy` - Fixed number of trusted proxies
* `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges
* `ChainStrategy` - Try multiple strategies with fallback
## Examples
```clojure
;; Single reverse proxy with X-Real-IP
(def app
(-> handler
(wrap-client-ip {:strategy (strategy/single-ip-header-strategy "x-real-ip")})
;; other middleware
))
;; Multiple proxies with private IPs
(def app
(-> handler
(wrap-client-ip {:strategy (strategy/rightmost-non-private-strategy "x-forwarded-for")})
;; other middleware
))
;; Chain strategy with fallback
(def app
(-> handler
(wrap-client-ip {:strategy
(strategy/chain-strategy
[(strategy/single-ip-header-strategy "x-real-ip")
(strategy/remote-addr-strategy)])})
;; other middleware
))
```
The middleware adds the `:ol/client-ip` key to the request map, containing the determined
client InetAddress or nil.
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/core.clj#L60-L121)
## ol.client-ip.ip
# ol.client-ip.ip
IP address utilities for conversion and classification.
This namespace provides functions for working with IP addresses,
particularly for converting between string, InetAddress, and numeric
representations. It follows RFC 5952 for IPv6 address formatting and
provides utilities for type checking and serialization.
**📌 NOTE**\
None of the functions in this namespace accept ip addreses input as strings.
Use [`ol.client-ip.parse-ip/from-string`](api/ol-client-ip-parse-ip.adoc#from-string) to convert strings to InetAddress objects.
The key operations available are:
* Type checking with `ipv4?` and `ipv6?`
* String formatting with `format-ip`
* Numeric conversion with `->numeric` and `numeric->`
These functions are designed to work with the `java.net.InetAddress`
class hierarchy and its subclasses `Inet4Address` and `Inet6Address`.
Refer to [`ol.client-ip.parse-ip`](api/ol-client-ip-parse-ip.adoc) and [`ol.client-ip.cidr`](api/ol-client-ip-cidr.adoc) for more useful functions.
## ipv4?
```clojure
(ipv4? ip)
```
Checks if an IP address is an IPv4 address.
This function determines if the given IP address is an instance of
`java.net.Inet4Address`.
Arguments:
ip - An IP address object (InetAddress)
Returns:
`true` if the IP address is IPv4, `false` otherwise
Examples:
```clojure
(ipv4? (parse-ip/from-string "192.168.1.1")) ;; => true
(ipv4? (parse-ip/from-string "::1")) ;; => false
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L28-L46)
---
## ipv6?
```clojure
(ipv6? ip)
```
Checks if an IP address is an IPv6 address.
This function determines if the given IP address is an instance of
`java.net.Inet6Address`.
Arguments:
ip - An IP address object (InetAddress)
Returns:
`true` if the IP address is IPv6, `false` otherwise
Examples:
```clojure
(ipv6? (parse-ip/from-string "2001:db8::1")) ;; => true
(ipv6? (parse-ip/from-string "192.168.1.1")) ;; => false
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L48-L66)
---
## format-ipv6
```clojure
(format-ipv6 ip)
```
Converts an IPv6 address to its canonical string representation.
This function formats IPv6 addresses according to RFC 5952, which includes:
- Using lowercase hexadecimal digits
- Compressing the longest run of consecutive zero fields with ::
- Not using :: to compress a single zero field
- Including scope IDs with % delimiter for link-local addresses
Arguments:
ip - An IPv6 address object (Inet6Address)
Returns:
A string representation of the IPv6 address formatted according to RFC 5952
Examples:
```clojure
(format-ipv6 (parse-ip/from-string "2001:db8:0:0:0:0:0:1"))
;; => "2001:db8::1"
(format-ipv6 (parse-ip/from-string "fe80::1%eth0"))
;; => "fe80::1%eth0"
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L121-L152)
---
## format-ip
```clojure
(format-ip ip)
```
Converts an IP address to its canonical string representation.
This function formats IP addresses according to standard conventions:
- IPv4 addresses use dotted decimal notation (e.g., "192.168.0.1")
- IPv6 addresses follow RFC 5952 with compressed notation using ::
for the longest run of zeros, and embedded IPv4 addresses where appropriate
The implementation handles scope IDs for IPv6 link-local addresses
and properly compresses IPv6 addresses according to the standard rules.
Arguments:
ip - An IP address object (InetAddress)
Returns:
A string representation of the IP address
Throws:
IllegalArgumentException - if the input is not a valid InetAddress
Examples:
```clojure
(format-ip (parse-ip/from-string "192.168.1.1")) ;; => "192.168.1.1"
(format-ip (parse-ip/from-string "2001:db8::1")) ;; => "2001:db8::1"
(format-ip (parse-ip/from-string "::ffff:1.2.3.4")) ;; => "::ffff:1.2.3.4"
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L154-L185)
---
## ->numeric
```clojure
(->numeric ip)
```
Converts an IP address to its numeric representation as a BigInteger.
This function takes an InetAddress (either IPv4 or IPv6) and returns
a BigInteger representing the address. The BigInteger can be used for
IP address arithmetic, comparison, or storage.
Arguments:
ip - An IP address object (InetAddress)
Returns:
A BigInteger representing the IP address
Examples:
```clojure
(->numeric (parse-ip/from-string "192.0.2.1"))
;; => 3221225985
(->numeric (parse-ip/from-string "2001:db8::1"))
;; => 42540766411282592856903984951653826561
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L187-L209)
---
## numeric->
```clojure
(numeric-> address version)
```
Converts a BigInteger to an InetAddress.
This function takes a numeric representation of an IP address as a BigInteger
and converts it back to an InetAddress object. The `ipv6?` flag determines
whether to create an IPv4 or IPv6 address.
Arguments:
address - A BigInteger representing the IP address
version - One of :v6 or :v4 indicating whether this is an IPv6 address
or an IPv4 address
Returns:
An InetAddress object representing the numeric address, or nil
Throws:
ExceptionInfo - If the BigInteger is negative or too large for the
specified address type
Examples:
```clojure
;; Convert back to IPv4
(numeric-> (BigInteger. "3221226113") :v4)
;; => #object[java.net.Inet4Address 0x578d1c5 "/192.0.2.129"]
;; Convert back to IPv6
(numeric-> (BigInteger. "42540766411282592856903984951653826561") :v6)
;; => #object[java.net.Inet6Address 0x14832c23 "/2001:db8:0:0:0:0:0:1"]
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L211-L267)
---
## numeric-v6->
```clojure
(numeric-v6-> address)
```
Converts a BigInteger to an IPv6 address.
This is a convenience wrapper around `numeric->` that automatically
sets the `ipv6?` flag to true.
Arguments:
address - A BigInteger representing the IPv6 address
Returns:
An Inet6Address object or nil if the input is invalid
See also: <<numeric--GT-,`numeric->`>>
Examples:
```clojure
(numeric-v6-> (BigInteger. "42540766411282592856903984951653826561"))
;; => #object[java.net.Inet6Address 0x42668812 "/2001:db8:0:0:0:0:0:1"]
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L269-L292)
---
## numeric-v4->
```clojure
(numeric-v4-> address)
```
Converts a BigInteger to an IPv4 address.
Arguments:
address - A BigInteger representing the IPv4 address
Returns:
An Inet4Address object or nil if the input is invalid
See also: <<numeric--GT-,`numeric->`>>
Examples:
```clojure
(numeric-v4-> (BigInteger. "3221226113"))
;; => #object[java.net.Inet4Address 0x6b88aeff "/192.0.2.129"]
```
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/ip.clj#L294-L314)
## ol.client-ip.parse-ip
# ol.client-ip.parse-ip
Parse a string representation of an IPv4 or IPv6 address into an InetAddress instance.
Unlike `InetAddress/getByName`, the functions in this namespace never cause
DNS services to be accessed. This avoids potentially blocking/side-effecting
network calls that can occur when using the JDK’s built-in methods to parse IP
addresses.
This implementation is inspired by Google Guava’s InetAddresses class, which
provides similar functionality in Java. This code focuses on strict validation
of IP address formats according to relevant RFCs.
Features:
- Non-blocking IP address parsing with pure functions
- Strict RFC-compliant validation
- Support for all IPv4 formats
- Support for all IPv6 formats (including compressed notation and embedded IPv4)
- Support for IPv6 scope identifiers
The main entry point is `parse-ip`, which takes a string and returns an
InetAddress instance or nil if the input is an invalid ip address literal.
## bytes->inet-address
```clojure
(bytes->inet-address addr)
(bytes->inet-address addr scope)
```
Convert a byte array into an InetAddress.
Args:
addr: the raw 4-byte or 16-byte IP address in big-endian order
scope: optional scope identifier for IPv6 addresses
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/parse_ip.clj#L231-L256)
---
## from-string
```clojure
(from-string ip-string)
```
Returns the InetAddress having the given string representation or nil otherwise.
This function parses IP address strings without performing DNS lookups, making it
suitable for environments where DNS lookups would cause unwanted blocking or side effects.
It supports:
- IPv4 addresses in dotted decimal format (e.g., "192.168.1.1")
- IPv6 addresses in hex format with optional compression (e.g., "2001:db8::1")
- IPv6 addresses with scope IDs (e.g., "fe80::1%eth0" or "fe80::1%1")
- IPv6 addresses with embedded IPv4 (e.g., "::ffff:192.168.1.1")
If the input is already an InetAddress, it is returned unchanged.
Args:
ip-string: A string representing an IP address or an InetAddress
Returns:
An InetAddress object, or nil if the input couldn’t be parsed
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/parse_ip.clj#L258-L290)
## ol.client-ip.protocols
# ol.client-ip.protocols
## ClientIPStrategy
Protocol for determining client IP from headers and remote address.
Returns InetAddress or nil if no valid client ip is found.
Implementations should:
* Be thread-safe and reusable across requests
* Validate configuration at creation time (throw on invalid config)
_protocol_
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/protocols.clj#L5-L21)
### client-ip
```clojure
(client-ip this headers remote-addr)
```
Extract the client IP from request headers and remote address.
Args:
headers: Ring-style headers map (lowercase keys)
remote-addr: String from Ring request :remote-addr
Returns:
InetAddress or nil if no valid client IP found.
## ol.client-ip.strategy
# ol.client-ip.strategy
Strategy implementations for determining client IP addresses.
This namespace provides various strategy implementations for extracting the
real client IP from HTTP headers and connection information.
Each strategy is designed for specific network configurations:
* `RemoteAddrStrategy` - Direct connections (no reverse proxy)
* `SingleIPHeaderStrategy` - Single trusted reverse proxy with single IP headers
* `RightmostNonPrivateStrategy` - Multiple proxies, all with private IPs
* `LeftmostNonPrivateStrategy` - Get IP closest to original client (not secure)
* `RightmostTrustedCountStrategy` - Fixed number of trusted proxies
* `RightmostTrustedRangeStrategy` - Known trusted proxy IP ranges
* `ChainStrategy` - Try multiple strategies in order
Strategies are created once and reused across requests. They are thread-safe
and designed to fail fast on configuration errors while being resilient to
malformed input during request processing.
## parse-forwarded
```clojure
(parse-forwarded header-value)
```
Parse a Forwarded header value according to RFC 7239.
Returns a sequence of InetAddress objects, or an empty sequence if none are valid.
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L119-L130)
---
## split-host-zone
```clojure
(split-host-zone ip-string)
```
Split IPv6 zone from IP address.
IPv6 addresses can include zone identifiers (scope IDs) separated by '%'.
For example: 'fe80::1%eth0' or 'fe80::1%1'
Args:
ip-string: String IP address that may contain a zone identifier
Returns:
Vector of [ip zone] where zone is the zone identifier string,
or [ip nil] if no zone is present.
Examples:
(split-host-zone "192.168.1.1") => ["192.168.1.1" nil]
(split-host-zone "fe80::1%eth0") => ["fe80::1" "eth0"]
(split-host-zone "fe80::1%1") => ["fe80::1" "1"]
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L150-L171)
---
## remote-addr-strategy
```clojure
(remote-addr-strategy)
```
Create a strategy that uses the direct client socket IP.
This strategy extracts the IP address from the request’s :remote-addr field,
which represents the direct client socket connection. Use this when your
server accepts direct connections from clients (not behind a reverse proxy).
The strategy strips any port information and validates the IP is not
zero/unspecified (0.0.0.0 or ::).
Returns:
RemoteAddrStrategy instance
Example:
(remote-addr-strategy)
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L184-L200)
---
## single-ip-header-strategy
```clojure
(single-ip-header-strategy header-name)
```
Create a strategy that extracts IP from headers containing a single IP address.
This strategy is designed for headers like X-Real-IP, CF-Connecting-IP,
True-Client-IP, etc. that contain only one IP address. Use this when you
have a single trusted reverse proxy that sets a reliable single-IP header.
The strategy validates that the header name is not X-Forwarded-For or
Forwarded, as these headers can contain multiple IPs and should use
different strategies.
Args:
header-name: String name of the header to check (case-insensitive)
Returns:
SingleIPHeaderStrategy instance
Throws:
ExceptionInfo if header-name is invalid or refers to multi-IP headers
Examples:
(single-ip-header-strategy "x-real-ip")
(single-ip-header-strategy "cf-connecting-ip")
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L208-L235)
---
## rightmost-non-private-strategy
```clojure
(rightmost-non-private-strategy header-name)
```
Create a strategy that gets the rightmost non-private IP from forwarding headers.
This strategy processes IPs in reverse order (rightmost first) and returns
the first IP that is not in a private/reserved range. Use this when all
your reverse proxies have private IP addresses, so the rightmost non-private
IP should be the real client IP.
The strategy supports X-Forwarded-For and Forwarded headers that contain
comma-separated IP lists.
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
Returns:
RightmostNonPrivateStrategy instance
Throws:
ExceptionInfo if header-name is invalid or refers to single-IP headers
Examples:
(rightmost-non-private-strategy "x-forwarded-for")
(rightmost-non-private-strategy "forwarded")
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L250-L278)
---
## leftmost-non-private-strategy
```clojure
(leftmost-non-private-strategy header-name)
```
Create a strategy that gets the leftmost non-private IP from forwarding headers.
This strategy processes IPs in forward order (leftmost first) and returns
the first IP that is not in a private/reserved range. This gets the IP
closest to the original client.
⚠️ ***WARNING: NOT FOR SECURITY USE*** ⚠️
This strategy is easily spoofable since clients can add arbitrary IPs to
the beginning of forwarding headers. Use only when security is not a concern
and you want the IP closest to the original client.
The strategy supports X-Forwarded-For and Forwarded headers that contain
comma-separated IP lists.
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
Returns:
LeftmostNonPrivateStrategy instance
Throws:
ExceptionInfo if header-name is invalid or refers to single-IP headers
Examples:
(leftmost-non-private-strategy "x-forwarded-for")
(leftmost-non-private-strategy "forwarded")
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L293-L325)
---
## rightmost-trusted-count-strategy
```clojure
(rightmost-trusted-count-strategy header-name trusted-count)
```
Create a strategy that returns IP at specific position based on known proxy count.
This strategy is for when you have a fixed number of trusted proxies and want
to get the IP at the specific position that represents the original client.
Given N trusted proxies, the client IP should be at position -(N+1) from the end.
For example, with 2 trusted proxies and header 'client, proxy1, proxy2, proxy3':
- Total IPs: 4
- Skip last 2 (trusted): positions 2,3
- Return IP at position 1 (proxy1)
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
trusted-count: Number of trusted proxies (must be > 0)
Returns:
RightmostTrustedCountStrategy instance
Throws:
ExceptionInfo if header-name is invalid or trusted-count <= 0
Examples:
(rightmost-trusted-count-strategy "x-forwarded-for" 1)
(rightmost-trusted-count-strategy "forwarded" 2)
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L338-L371)
---
## rightmost-trusted-range-strategy
```clojure
(rightmost-trusted-range-strategy header-name trusted-ranges)
```
Create a strategy that returns rightmost IP not in trusted ranges.
This strategy processes IPs in reverse order (rightmost first) and returns
the first IP that is not within any of the specified trusted IP ranges.
Use this when you know the specific IP ranges of your trusted proxies.
The strategy supports X-Forwarded-For and Forwarded headers that contain
comma-separated IP lists.
Args:
header-name: String name of the header to check (case-insensitive)
Should be "x-forwarded-for" or "forwarded"
trusted-ranges: Collection of CIDR ranges (strings) that represent trusted proxies
Each range should be a valid CIDR notation like "192.168.1.0/24"
Returns:
RightmostTrustedRangeStrategy instance
Throws:
ExceptionInfo if header-name is invalid or trusted-ranges is empty/invalid
Examples:
(rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8" "192.168.0.0/16"])
(rightmost-trusted-range-strategy "forwarded" ["172.16.0.0/12"])
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L386-L424)
---
## chain-strategy
```clojure
(chain-strategy strategies)
```
Create a strategy that tries multiple strategies in order until one succeeds.
This strategy allows fallback scenarios where you want to try different
IP detection methods in priority order. Each strategy is tried in sequence
until one returns a non-empty result.
Common use case: Try a single-IP header first, then fall back to remote address.
Args:
strategies: Vector of strategy instances to try in order
Must contain at least one strategy
Returns:
ChainStrategy instance
Throws:
ExceptionInfo if strategies is empty or contains invalid strategies
Examples:
(chain-strategy [(single-ip-header-strategy "x-real-ip")
(remote-addr-strategy)])
(chain-strategy [(rightmost-non-private-strategy "x-forwarded-for")
(single-ip-header-strategy "x-real-ip")
(remote-addr-strategy)])
[source,window=_blank](https://github.com/outskirtslabs/client-ip/blob/main/src/ol/client_ip/strategy.clj#L435-L470)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## ++[++UNRELEASED++]++
## `v0.1.1` (2025-08-18)
Sorry, this is my first from-scratch clojars release, and I’m struggling
to get the metadata working.
There are no changes.
## `v0.1.0` (2025-08-18)
This is the first public release of this codebase under the
`ol.client-ip` moniker.
Variations of this code has been used in production for a long time, but
nonetheless I want to be cautious so we’re starting with sub-1.0
versioning.
Please report any problems and let me know if anything is unclear,
inconvenient, etc. Thank you! 🙏
## ol.client-ip
# ol.client-ip
> A 0-dependency ring middleware for determining a request’s real client IP address from HTTP headers




`X-Forwarded-For` and other client IP headers are [often used incorrectly](https://adam-p.ca/blog/2022/03/x-forwarded-for/), resulting in bugs and security vulnerabilities. This library provides strategies for extracting the correct client IP based on your network configuration.
It is based on the golang reference implementation [realclientip/realclientip-go](https://github.com/realclientip/realclientip-go).
Quick feature list:
* ring middleware determining the client’s IP address
* 0 dependency IP address string parsing with guaranteed no trips to the hosts' DNS services, which can block and timeout (unlike Java’s `InetAddress/getByName`)
* rightmost-ish strategies support the `X-Forwarded-For` and `Forwarded` (RFC 7239) headers
* IPv6 zone identifiers support
Note that there is no dependency on ring, the public API could also be used for pedestal or sieppari-style interceptors.
Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/client-ip {:mvn/version "0.1.1"}}}
;; Leiningen
[com.outskirtslabs/client-ip "0.1.1"]
```
## Quick Start
```clojure
(ns myapp.core
(:require [ol.client-ip.core :as client-ip]
[ol.client-ip.strategy :as strategy]))
;; Simple case: behind a trusted proxy that sets X-Real-IP
(def app
(-> handler
(client-ip/wrap-client-ip
{:strategy (strategy/single-ip-header-strategy "x-real-ip")})))
;; The client IP is now available in the request
(defn handler [request]
(let [client-ip (:ol/client-ip request)]
{:status 200
:body (str "Your IP is: " client-ip)}))
```
For detailed guidance on choosing strategies, see [Usage Guide](usage.adoc).
Choosing the wrong strategy can result in IP address spoofing security vulnerabilities.
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.client-ip/0.1/)
* [API Reference](https://docs.outskirtslabs.com/ol.client-ip/0.1/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/client-ip/issues)
## Recommended Reading
You think it is an easy question:
> I have an HTTP application, I just want to know the IP address of my client.
But who is the client?
> The computer on the other end of the network connection?
But which network connection? The one connected to your HTTP application is probably a reverse proxy or load balancer.
> Well I mean the "user’s IP address"
It ain’t so easy kid.
There are many good articles on the internet that discuss the perils and pitfalls of trying to answer this deceptively simple question.
You _should_ read one or two of them to get an idea of the complexity in this space. Libraries, like this one, _cannot_ hide the complexity from you, there is no abstraction nor encapsulation nor "default best practice".
Below are some of those good articles:
* MDN: [X-Forwarded-For: Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#security_and_privacy_concerns)
* [The perils of the "real" client IP](https://adam-p.ca/blog/2022/03/x-forwarded-for/) ([archive link](https://web.archive.org/web/20250416042714/https://adam-p.ca/blog/2022/03/x-forwarded-for/))
* [Rails IP Spoofing Vulnerabilities and Protection](https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) ([archive link](https://web.archive.org/web/20250421121810/https://www.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/))
* [ol.client-ip: A Clojure Library to Prevent IP Spoofing](https://casey.link/blog/client-ip-ring-middleware/)
## Security
See [here](https://github.com/outskirtslabs/client-ip/security/advisories) for security advisories or to report a security vulnerability.
## License
Copyright (C) 2025 Casey Link <casey@outskirtslabs.com>
Distributed under the [MIT License](https://github.com/outskirtslabs/client-ip/blob/main/LICENSE).
## Security policy
# Security policy
## Advisories
All security advisories will be posted
https://github.com/outskirtslabs/client-ip/security/advisories[on
GitHub].
## Reporting a vulnerability
Please report possible security vulnerabilities
https://github.com/outskirtslabs/client-ip/security/advisories[via
GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt
emails with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see
[Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
— [Casey Link](https://casey.link)
## Usage Guide
# Usage Guide
This guide explains when and why to use each strategy. The choice of strategy is
critical -- _using the wrong strategy can lead to IP spoofing vulnerabilities_.
## Understanding Your Network Setup
Before choosing a strategy, you need to understand your production environment’s network topology:
1. Are you behind reverse proxies? (nginx, Apache, load balancers, CDNs)
2. How many trusted proxies are there?
3. What headers do they set?
The answers determine which strategy is appropriate and secure for your setup.
## Terminology
* **socket-level IP**\
The IP address that a server or proxy observes at the TCP socket level from whatever is directly connecting to it. This is always trustworthy since it cannot be spoofed at the socket level, regardless of whether the connection is from an end user or another proxy in the chain.
* **private IP addresses**\
IP addresses reserved for private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) that are not routable on the public internet. These typically represent your internal infrastructure components.
* **reverse proxy / proxy / load balancer / CDN**\
An intermediary server that sits between clients and your application server, forwarding client requests. Examples include: nginx, Apache, caddy, AWS ALB, Cloudflare, Cloudfront, Fastly, Akamai, Fly.io, Hetzner Load Balancer.
* **trusted proxy**\
A reverse proxy under your control or operated by a service you trust, whose IP forwarding headers you can rely on. The key characteristic is that you know what headers it sets and clients cannot bypass it.
* **IP spoofing**\
The practice of sending packets with a forged source IP address, or in the context of HTTP headers, setting fake client IP values in headers like `X-Forwarded-For` to deceive the application about the request’s true origin.
* **trivially spoofable**\
Headers or IP values that can be easily faked by any client without special network access. For example, any client can send `X-Forwarded-For: 1.2.3.4` in their request.
* **XFF**\
Short for `X-Forwarded-For`, the most common HTTP header used by proxies to communicate the original client IP address through a chain of proxies. Each proxy typically appends the IP it received the request from.
* **rightmost-ish**\
A parsing strategy that extracts IP addresses from the right side of forwarding headers like `X-Forwarded-For`, typically skipping a known number of trusted proxies to find the IP added by the first trusted proxy. This is the only trustworthy approach since you control what your trusted proxies add to the header chain ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)).
* **leftmost-ish**\
A parsing strategy that takes the leftmost (first) IP address in forwarding headers, representing the alleged original client. This is highly untrustworthy and vulnerable to spoofing since any client can set arbitrary values at the beginning of these headers ([see this article](https://adam-p.ca/blog/2022/03/x-forwarded-for)).
## Strategy Reference
### Remote Address Strategy / No middleware at all!
* **When to use**\
Your server accepts direct connections from clients without any reverse proxies or load balancers.
* **Why**\
The socket-level remote address is the actual client IP when there are no intermediaries.
Using this library you can use the remote-addr-strategy, which is provided to be comprehensive, however you can also just use the existing [`:remote-addr`](https://github.com/ring-clojure/ring/blob/master/SPEC.md#remote-addr) key in your ring request map.
```clojure
(strategy/remote-addr-strategy)
;; OR, just fetch the remote addr from the ring request map
(:remote-addr request)
```
* **Example network**
```
Client -> Your Server
```
* **Security**\
Safe -- the remote address cannot be spoofed at the socket level.
---
### Single IP Header Strategy
* **When to use**\
You have a trusted reverse proxy that accepts connections directly from clients and sets a specific header with the client IP.
* **Why**\
Many Cloud Provider’s CDNs and load balancers products provide a single authoritative header with the real client IP. This is the most secure approach when you have such a setup.
* **Provider headers with trusted values**
* Cloudflare (everyone): `cf-connecting-ip` -- this is a socket-level IP value ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/))
* Cloudflare (enterprise): `true-client-ip` -- also a socket-level IP value, and just for Enterprise customers with backwards compatibility requirements ([docs](https://developers.cloudflare.com/fundamentals/get-started/http-request-headers/))
* Fly.io: `fly-client-ip` -- the socket ip address ([docs](https://www.fly.io/docs/networking/request-headers/#fly-client-ip))
* Azure FrontDoor: `x-azure-socketip` -- the socket IP address associated with the TCP connection that the current request originated from ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol))
* nginx with correctly configured [`ngx_http_realip_module`](https://nginx.org/en/docs/http/ngx_http_realip_module.html)
* Do not use `proxy_set_header X-Real-IP $remote_addr;`
* **Provider headers that require extra config**
These providers offer headers that out of the box are trivially spoofable, and require extra configuration (in your provider’s management interface) to configure securely.
* Akamai: `true-client-ip` -- trivially spoofable by default, refer to [this writeup](https://adam-p.ca/blog/2022/03/x-forwarded-for/#akamai)
* Fastly: `fastly-client-ip` -- trivially spoofable, you must use vcl to configure it [fastly docs](https://www.fastly.com/documentation/reference/http/http-headers/Fastly-Client-IP/)
* **Provider headers to avoid**
**⚠️ WARNING**\
In nearly all of these cases you are better off using XFF and reasoning about the number of proxies or their network addresses.
* Azure FrontDoor: `x-azure-clientip` -- trivially spoofable as it is the leftmost-ish XFF ([docs](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol))
```clojure
;; Clients connect to your application server *only* through and *directly* through Cloudflare
(strategy/single-ip-header-strategy "cf-connecting-ip")
;; Clients connect to your application server *only* through and *directly* through Fly.io
(strategy/single-ip-header-strategy "fly-client-ip")
```
* **Example networks**
```
Client -> Cloudflare -> Your Server
Client -> nginx -> Your Server
Client -> Load Balancer -> Your Server
```
* **Security considerations**
* Ensure the header cannot be spoofed by clients
* Verify clients cannot bypass your proxy
* Use only the header _your_ trusted proxy sets
**⚠️ WARNING**\
Common mistakes
* Using `x-forwarded-for` with this strategy (use rightmost strategies instead)
* Not validating that clients must go through your proxy
* Changes to your topology that break a rule that was previously true
* For example, adding another proxy in the chain or exposing servers on a different interface
* Using headers that can be set by untrusted sources
---
### Rightmost Non-Private Strategy
* **When to use**\
You have multiple reverse proxies between the internet and the server, all using private IP addresses, and they append to `X-Forwarded-For` or `Forwarded` headers.
* **Why**\
In a typical private network setup, the rightmost non-private IP in the trusted forwarding chain is the real client IP. Private IPs represent your infrastructure.
```clojure
(strategy/rightmost-non-private-strategy "x-forwarded-for")
;; Using RFC 7239 Forwarded header
(strategy/rightmost-non-private-strategy "forwarded")
```
* **Example network**
```
Client -> Internet Proxy -> Private Load Balancer -> Private App Server
(1.2.3.4) (10.0.1.1) (10.0.2.1)
X-Forwarded-For: 1.2.3.4, 10.0.1.1
Result: 1.2.3.4 (rightmost non-private)
```
* **Security**\
Secure. Attackers can still spoof the leftmost entries, but not the rightmost non-private IP.
**⚠️ WARNING**\
Common Mistakes
* Do not use when your proxies have public IP addresses
* Do not use when you need to trust specific proxy IPs (use trusted range strategy instead)
---
### Rightmost Trusted Count Strategy
* **When to use**\
You know exactly how many trusted proxies append IPs to the header, and you want the IP added by the first trusted proxy.
* **Why**\
When you have a fixed, known number of trusted proxies, counting backwards gives you the IP that was added by your first trusted proxy (the client IP it saw).
```clojure
;; Two trusted proxies
(strategy/rightmost-trusted-count-strategy "x-forwarded-for" 2)
;; One trusted proxy
(strategy/rightmost-trusted-count-strategy "forwarded" 1)
```
* **Example with count=2**
```
Client -> Proxy1 -> Proxy2 -> Your Server
(adds A) (adds B)
X-Forwarded-For: A, B
With count=2: Skip 2 from right, return A
```
* **Security**\
Secure, when your proxy count is stable and known.
**⚠️ WARNING**\
Common Mistakes
* Count must exactly match your trusted proxy count
* If the count is wrong, you’ll get incorrect results or errors
* Network topology changes require updating the count on the application server
---
### Rightmost Trusted Range Strategy
* **When to use**\
You know the IP ranges of all your trusted proxies and want the rightmost IP that’s not from a trusted source.
* **Why**\
This is the most flexible strategy for complex infrastructures where you know your proxy IPs but they might change within known ranges.
```clojure
;; You have a VPC where your client-facing load balancer could be any ip address
;; inside the 10.1.1.0/24 subnet (az1) or 10.1.2.0/24 (az2)
(strategy/rightmost-trusted-range-strategy
"x-forwarded-for"
["10.1.1.0/24" "10.1.2.0/24"])
;; Including Cloudflare ranges (these are examples, do not copy them!)
(strategy/rightmost-trusted-range-strategy
"x-forwarded-for"
["173.245.48.0/20" "103.21.244.0/22" ; Example Cloudflare IPv4 ranges
"2400:cb00::/32"]) ; Example Cloudflare IPv6 range
```
* **Example**
```
Client -> Cloudflare -> Your Load Balancer -> App Server
1.2.3.4 173.245.48.1 10.0.1.1
X-Forwarded-For: 1.2.3.4, 173.245.48.1, 10.0.1.1
Trusted ranges: ["173.245.48.0/20", "10.0.0.0/8"]
Result: 1.2.3.4 (rightmost IP not in trusted ranges)
```
* **Security**\
Secure, when ranges are properly maintained.
**⚠️ WARNING**\
Common Mistakes
* Forgetting to keep the trusted ranges up to date
* Not including all possible proxy IPs
* When using a cloud provider, not using their API for current up to date ranges
---
### Chain Strategy
* **When to use**\
You have multiple possible network paths to your server and need fallback behavior.
**⚠️ WARNING**\
Do not abuse ChainStrategy to check multiple headers. There is likely only one header you should be checking, and checking more can leave you vulnerable to IP spoofing. Each strategy should represent a different network path, not multiple ways to parse the same path.
* **Why**\
Real-world deployments can have multiple possible configurations (direct connections or via CDN). Chain strategy tries each approach until one succeeds.
```clojure
;; Clients can connect via cloudflare or directly to your server
(strategy/chain-strategy
[(strategy/single-ip-header-strategy "cf-connecting-ip")
(strategy/remote-addr-strategy)])
;; Multiple fallback levels
(strategy/chain-strategy
[(strategy/rightmost-trusted-range-strategy "x-forwarded-for" ["10.0.0.0/8"])
(strategy/rightmost-non-private-strategy "x-forwarded-for")
(strategy/remote-addr-strategy)])
```
* **Example use cases**
* Development vs production environments
* Gradual migration between proxy setups
* Handling both CDN and direct traffic
---
### Leftmost Non-Private Strategy
**⚠️ WARNING**\
DO NOT USE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING. The leftmost IP can be trivially spoofed by clients.
* **When to use**\
Rarely recommended. Only when you specifically need the IP allegedly closest to the original client (knowing full well that it can be spoofed).
* **Why**\
This gives you the "apparent" client IP but offers _no security_ against spoofing.
```clojure
(strategy/leftmost-non-private-strategy "x-forwarded-for")
(strategy/leftmost-non-private-strategy "forwarded")
```
* **Example**
```
X-Forwarded-For: 1.2.3.4, 2.3.4.5, 192.168.1.1
Result: 1.2.3.4 (leftmost non-private)
```
* **Valid use cases**
* Debugging or logging where you want the "claimed" client IP
* Analytics where approximate location matters more than accuracy
## Testing Your Configuration
Before deploying features that rely on the client IP address, deploy this
middleware with logging into your production network and verify your strategy
works correctly:
1. Test with expected traffic: Ensure you get the right IP for normal requests
2. Test spoofing attempts: Verify that fake headers are ignored, you can spoof headers easily with `curl -H "X-Forwarded-For: 1.2.3.4" ...`
3. Monitor for empty results: The middleware returns `nil` when a failure occurs, this could indicate an attack or a configuration problem.
## Common Pitfalls
1. Using multiple headers: Never chain strategies that check different headers from the same request
2. Wrong header choice: For example, using `x-forwarded-for` when your proxy sets `x-real-ip`
3. Ignoring network changes: Proxy counts or ranges change but your app configuration doesn’t
4. Development vs production: Different strategies needed for different environments
5. Not validating proxy control: Assuming headers can’t be spoofed when they can, don’t just trust the hyperscalers or commercial CDNs, _verify_.
Remember: _the right strategy depends entirely on your specific network configuration_. When in doubt, analyze real traffic in a real network setting.
## ol.dirs
# ol.dirs
_platforms: clj, cljs_
Cross-platform directory lookup for Clojure, ClojureScript on Node, and ClojureDart.
The public API returns strings or vectors of strings and never creates directories.
* arity 0 returns the base directory
* arity 1 appends an application name
* arity 2 appends qualifier, organization, and application using platform rules
See [`config-home`](#config-home), [`data-home`](#data-home), [`state-home`](#state-home), and [`home-dir`](#home-dir).
## data-home
### clj
_platforms: clj_
```clojure
(data-home)
(data-home application)
(data-home qualifier organization application)
```
Returns the writable user data directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L14-L19)
### cljs
_platforms: cljs_
```clojure
(data-home)
(data-home application)
(data-home qualifier organization application)
```
Returns the writable user data directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L14-L19)
---
## config-home
### clj
_platforms: clj_
```clojure
(config-home)
(config-home application)
(config-home qualifier organization application)
```
Returns the writable user configuration directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L21-L26)
### cljs
_platforms: cljs_
```clojure
(config-home)
(config-home application)
(config-home qualifier organization application)
```
Returns the writable user configuration directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L21-L26)
---
## state-home
### clj
_platforms: clj_
```clojure
(state-home)
(state-home application)
(state-home qualifier organization application)
```
Returns the writable user state directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L28-L33)
### cljs
_platforms: cljs_
```clojure
(state-home)
(state-home application)
(state-home qualifier organization application)
```
Returns the writable user state directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L28-L33)
---
## data-dirs
### clj
_platforms: clj_
```clojure
(data-dirs)
(data-dirs application)
(data-dirs qualifier organization application)
```
Returns the shared data search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L35-L40)
### cljs
_platforms: cljs_
```clojure
(data-dirs)
(data-dirs application)
(data-dirs qualifier organization application)
```
Returns the shared data search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L35-L40)
---
## config-dirs
### clj
_platforms: clj_
```clojure
(config-dirs)
(config-dirs application)
(config-dirs qualifier organization application)
```
Returns the shared configuration search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L42-L47)
### cljs
_platforms: cljs_
```clojure
(config-dirs)
(config-dirs application)
(config-dirs qualifier organization application)
```
Returns the shared configuration search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L42-L47)
---
## cache-home
### clj
_platforms: clj_
```clojure
(cache-home)
(cache-home application)
(cache-home qualifier organization application)
```
Returns the writable user cache directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L49-L54)
### cljs
_platforms: cljs_
```clojure
(cache-home)
(cache-home application)
(cache-home qualifier organization application)
```
Returns the writable user cache directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L49-L54)
---
## runtime-dir
### clj
_platforms: clj_
```clojure
(runtime-dir)
(runtime-dir application)
(runtime-dir qualifier organization application)
```
Returns the runtime directory, or `nil` on platforms without one.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L56-L61)
### cljs
_platforms: cljs_
```clojure
(runtime-dir)
(runtime-dir application)
(runtime-dir qualifier organization application)
```
Returns the runtime directory, or `nil` on platforms without one.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L56-L61)
---
## executable-dir
### clj
_platforms: clj_
```clojure
(executable-dir)
(executable-dir application)
(executable-dir qualifier organization application)
```
Returns the user executable directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L63-L68)
### cljs
_platforms: cljs_
```clojure
(executable-dir)
(executable-dir application)
(executable-dir qualifier organization application)
```
Returns the user executable directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L63-L68)
---
## preference-dir
### clj
_platforms: clj_
```clojure
(preference-dir)
(preference-dir application)
(preference-dir qualifier organization application)
```
Returns the preference directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L70-L75)
### cljs
_platforms: cljs_
```clojure
(preference-dir)
(preference-dir application)
(preference-dir qualifier organization application)
```
Returns the preference directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L70-L75)
---
## home-dir
### clj
_platforms: clj_
```clojure
(home-dir)
```
Returns the user’s home directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L77-L79)
### cljs
_platforms: cljs_
```clojure
(home-dir)
```
Returns the user’s home directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L77-L79)
---
## audio-dir
### clj
_platforms: clj_
```clojure
(audio-dir)
```
Returns the user’s audio or music directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L81-L83)
### cljs
_platforms: cljs_
```clojure
(audio-dir)
```
Returns the user’s audio or music directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L81-L83)
---
## desktop-dir
### clj
_platforms: clj_
```clojure
(desktop-dir)
```
Returns the user’s desktop directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L85-L87)
### cljs
_platforms: cljs_
```clojure
(desktop-dir)
```
Returns the user’s desktop directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L85-L87)
---
## document-dir
### clj
_platforms: clj_
```clojure
(document-dir)
```
Returns the user’s documents directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L89-L91)
### cljs
_platforms: cljs_
```clojure
(document-dir)
```
Returns the user’s documents directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L89-L91)
---
## download-dir
### clj
_platforms: clj_
```clojure
(download-dir)
```
Returns the user’s downloads directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L93-L95)
### cljs
_platforms: cljs_
```clojure
(download-dir)
```
Returns the user’s downloads directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L93-L95)
---
## font-dir
### clj
_platforms: clj_
```clojure
(font-dir)
```
Returns the user’s font directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L97-L99)
### cljs
_platforms: cljs_
```clojure
(font-dir)
```
Returns the user’s font directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L97-L99)
---
## picture-dir
### clj
_platforms: clj_
```clojure
(picture-dir)
```
Returns the user’s pictures directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L101-L103)
### cljs
_platforms: cljs_
```clojure
(picture-dir)
```
Returns the user’s pictures directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L101-L103)
---
## public-dir
### clj
_platforms: clj_
```clojure
(public-dir)
```
Returns the user’s public sharing directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L105-L107)
### cljs
_platforms: cljs_
```clojure
(public-dir)
```
Returns the user’s public sharing directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L105-L107)
---
## template-dir
### clj
_platforms: clj_
```clojure
(template-dir)
```
Returns the user’s templates directory, or `nil` when unsupported or unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L109-L111)
### cljs
_platforms: cljs_
```clojure
(template-dir)
```
Returns the user’s templates directory, or `nil` when unsupported or unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L109-L111)
---
## video-dir
### clj
_platforms: clj_
```clojure
(video-dir)
```
Returns the user’s videos directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L113-L115)
### cljs
_platforms: cljs_
```clojure
(video-dir)
```
Returns the user’s videos directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L113-L115)
---
## state-dir
### clj
_platforms: clj_
```clojure
(state-dir)
(state-dir application)
(state-dir qualifier organization application)
```
Alias of [`state-home`](#state-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L117-L122)
### cljs
_platforms: cljs_
```clojure
(state-dir)
(state-dir application)
(state-dir qualifier organization application)
```
Alias of [`state-home`](#state-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L117-L122)
---
## config-dir
### clj
_platforms: clj_
```clojure
(config-dir)
(config-dir application)
(config-dir qualifier organization application)
```
Alias of [`config-home`](#config-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L124-L129)
### cljs
_platforms: cljs_
```clojure
(config-dir)
(config-dir application)
(config-dir qualifier organization application)
```
Alias of [`config-home`](#config-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/v0.1.x/src/ol/dirs.cljc#L124-L129)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning).
## ++[++UNRELEASED++]++
## `v0.1.0` (2026-03-12)
This is the first public release of this codebase under the `ol.dirs` name.
This release establishes the first published cross-platform directory discovery API for Clojure, ClojureScript on Node, and ClojureDart.
Private variations of the clj/JVM code has been used in production for a awhile, but nonetheless I want to be cautious so we’re starting with sub-1.0 versioning.
## ol.dirs
# ol.dirs
> A 0-dependency implementation of XDG base directories, systemd unit directories, Windows Known Folder paths, and macOS standard directories for Clojure, ClojureScript, and ClojureDart.


[(https://img.shields.io/clojars/v/com.outskirtslabs/dirs.svg)]
`ol.dirs` returns string paths or vectors of string paths. It does not create directories, does not check whether they exist, and carries no runtime dependencies.
## Support Matrix
| Runtime | Linux | macOS | Windows |
| --- | --- | --- | --- |
| Clojure | yes | yes | yes |
| ClojureScript on Node | yes | yes | yes |
| ClojureDart | yes | yes | yes |
Project status: **[Maturing](https://docs.outskirtslabs.com/open-source-vital-signs#maturing)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/dirs {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/dirs "0.1.0"]
```
## Quick Start
```clojure
(require '[ol.dirs :as dirs])
(dirs/config-home)
;; => "/home/alice/.config"
(dirs/config-home "My App")
;; => "/home/alice/.config/my-app"
(dirs/config-home "org" "Acme Corp" "My App")
;; => Linux: "/home/alice/.config/my-app"
;; => macOS: "/Users/Alice/Library/Application Support/org.Acme-Corp.My-App"
;; => Windows: "C:\\Users\\Alice\\AppData\\Roaming\\Acme Corp\\My App"
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.dirs/0.1/)
* [API Reference](https://docs.outskirtslabs.com/ol.dirs/0.1/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/dirs/issues)
## API Notes
These functions support app-specific arities. Arity `0` returns the base directory, arity `1` appends an application name, and arity `3` appends a platform-specific qualifier, organization, and application path. The concrete path shapes are shown below in the `App Path Rules` table and in the example above.
`data-home`, `config-home`, `state-home`, `cache-home`, `runtime-dir`, `executable-dir`, `preference-dir`, `state-dir`, and `config-dir` all support those same arities.
`data-dirs` and `config-dirs` do too, but return vectors and append the application path to each search root.
### App Path Rules
| Form | Linux | macOS | Windows |
| --- | --- | --- | --- |
| `("My App")` | `my-app` | `My-App` | `My App` |
| `("org" "Acme Corp" "My App")` | `my-app` | `org.Acme-Corp.My-App` | `Acme Corp\My App` |
### Base Directories
| Function | Linux | macOS | Windows |
| --- | --- | --- | --- |
| `home-dir` | `$HOME` | `$HOME` | `%USERPROFILE%` |
| `data-home` | `$XDG_DATA_HOME` or `$HOME/.local/share` | `$HOME/Library/Application Support` | `%AppData%` |
| `config-home` | `$XDG_CONFIG_HOME` or `$HOME/.config` | `$HOME/Library/Application Support` | `%AppData%` |
| `state-home` | `$XDG_STATE_HOME` or `$HOME/.local/state` | `$HOME/Library/Application Support` | `%LocalAppData%` |
| `cache-home` | `$XDG_CACHE_HOME` or `$HOME/.cache` | `$HOME/Library/Caches` | `%LocalAppData%` |
| `data-dirs` | `$XDG_DATA_DIRS` or `/usr/local/share`, `/usr/share` | `/Library/Application Support` | `%ProgramData%` |
| `config-dirs` | `$XDG_CONFIG_DIRS` or `/etc/xdg` | `/Library/Application Support` | `%ProgramData%` |
| `preference-dir` | same as `config-home` | `$HOME/Library/Preferences` | `%AppData%` |
| `runtime-dir` | `$XDG_RUNTIME_DIR` or `nil` | `nil` | `nil` |
| `executable-dir` | `$XDG_BIN_HOME` or `$XDG_DATA_HOME/../bin` | `nil` | `nil` |
On Linux, trusted systemd units can override `config-home`, `state-home`, and `cache-home` via `CONFIGURATION_DIRECTORY`, `STATE_DIRECTORY`, and `CACHE_DIRECTORY` as documented in [`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html).
### User-Facing Directories
| Function | Linux | macOS | Windows |
| --- | --- | --- | --- |
| `audio-dir` | `XDG_MUSIC_DIR` from `user-dirs.dirs` | `$HOME/Music` | Music Known Folder |
| `desktop-dir` | `XDG_DESKTOP_DIR` from `user-dirs.dirs` | `$HOME/Desktop` | Desktop Known Folder |
| `document-dir` | `XDG_DOCUMENTS_DIR` from `user-dirs.dirs` | `$HOME/Documents` | Documents Known Folder |
| `download-dir` | `XDG_DOWNLOAD_DIR` from `user-dirs.dirs` | `$HOME/Downloads` | Downloads Known Folder |
| `font-dir` | `$XDG_DATA_HOME/fonts` | `$HOME/Library/Fonts` | `nil` |
| `picture-dir` | `XDG_PICTURES_DIR` from `user-dirs.dirs` | `$HOME/Pictures` | Pictures Known Folder |
| `public-dir` | `XDG_PUBLICSHARE_DIR` from `user-dirs.dirs` | `$HOME/Public` | Public Known Folder |
| `template-dir` | `XDG_TEMPLATES_DIR` from `user-dirs.dirs` | `nil` | Templates Known Folder |
| `video-dir` | `XDG_VIDEOS_DIR` from `user-dirs.dirs` | `$HOME/Movies` | Videos Known Folder |
## Nil Semantics
These functions may return `nil` by design:
* `runtime-dir` and `executable-dir` on non-Linux platforms
* `font-dir` on Windows
* `template-dir` on macOS
* Linux user-facing directories when `user-dirs.dirs` is missing, unreadable, disabled, or malformed
## Development
```bash
bb test:clj
bb test:cljs
bb test:cljd
bb test
```
The CLJD workspace commands live under [`dart/`](dart/), because the Dart toolchain emits generated files that are easier to keep isolated there.
## License
Copyright (C) 2026 Casey Link
Distributed under the [EUPL-1.2](https://spdx.org/licenses/EUPL-1.2.html).
## Security policy
# Security policy
## Advisories
All security advisories for `ol.dirs` will be posted [on GitHub](https://github.com/outskirtslabs/dirs/security/advisories).
## Reporting a vulnerability
Please report possible security vulnerabilities [via GitHub](https://github.com/outskirtslabs/dirs/security/advisories), or by emailing me at `casey@outskirtslabs.com`. You may encrypt email with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
[Casey Link](https://casey.link)
## ol.dirs
# ol.dirs
_platforms: clj, cljs_
Cross-platform directory lookup for Clojure, ClojureScript on Node, and ClojureDart.
The public API returns strings or vectors of strings and never creates directories.
* arity 0 returns the base directory
* arity 1 appends an application name
* arity 2 appends qualifier, organization, and application using platform rules
See [`config-home`](#config-home), [`data-home`](#data-home), [`state-home`](#state-home), and [`home-dir`](#home-dir).
## data-home
### clj
_platforms: clj_
```clojure
(data-home)
(data-home application)
(data-home qualifier organization application)
```
Returns the writable user data directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L14-L19)
### cljs
_platforms: cljs_
```clojure
(data-home)
(data-home application)
(data-home qualifier organization application)
```
Returns the writable user data directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L14-L19)
---
## config-home
### clj
_platforms: clj_
```clojure
(config-home)
(config-home application)
(config-home qualifier organization application)
```
Returns the writable user configuration directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L21-L26)
### cljs
_platforms: cljs_
```clojure
(config-home)
(config-home application)
(config-home qualifier organization application)
```
Returns the writable user configuration directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L21-L26)
---
## state-home
### clj
_platforms: clj_
```clojure
(state-home)
(state-home application)
(state-home qualifier organization application)
```
Returns the writable user state directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L28-L33)
### cljs
_platforms: cljs_
```clojure
(state-home)
(state-home application)
(state-home qualifier organization application)
```
Returns the writable user state directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L28-L33)
---
## data-dirs
### clj
_platforms: clj_
```clojure
(data-dirs)
(data-dirs application)
(data-dirs qualifier organization application)
```
Returns the shared data search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L35-L40)
### cljs
_platforms: cljs_
```clojure
(data-dirs)
(data-dirs application)
(data-dirs qualifier organization application)
```
Returns the shared data search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L35-L40)
---
## config-dirs
### clj
_platforms: clj_
```clojure
(config-dirs)
(config-dirs application)
(config-dirs qualifier organization application)
```
Returns the shared configuration search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L42-L47)
### cljs
_platforms: cljs_
```clojure
(config-dirs)
(config-dirs application)
(config-dirs qualifier organization application)
```
Returns the shared configuration search roots as a vector of strings.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L42-L47)
---
## cache-home
### clj
_platforms: clj_
```clojure
(cache-home)
(cache-home application)
(cache-home qualifier organization application)
```
Returns the writable user cache directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L49-L54)
### cljs
_platforms: cljs_
```clojure
(cache-home)
(cache-home application)
(cache-home qualifier organization application)
```
Returns the writable user cache directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L49-L54)
---
## runtime-dir
### clj
_platforms: clj_
```clojure
(runtime-dir)
(runtime-dir application)
(runtime-dir qualifier organization application)
```
Returns the runtime directory, or `nil` on platforms without one.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L56-L61)
### cljs
_platforms: cljs_
```clojure
(runtime-dir)
(runtime-dir application)
(runtime-dir qualifier organization application)
```
Returns the runtime directory, or `nil` on platforms without one.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L56-L61)
---
## executable-dir
### clj
_platforms: clj_
```clojure
(executable-dir)
(executable-dir application)
(executable-dir qualifier organization application)
```
Returns the user executable directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L63-L68)
### cljs
_platforms: cljs_
```clojure
(executable-dir)
(executable-dir application)
(executable-dir qualifier organization application)
```
Returns the user executable directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L63-L68)
---
## preference-dir
### clj
_platforms: clj_
```clojure
(preference-dir)
(preference-dir application)
(preference-dir qualifier organization application)
```
Returns the preference directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L70-L75)
### cljs
_platforms: cljs_
```clojure
(preference-dir)
(preference-dir application)
(preference-dir qualifier organization application)
```
Returns the preference directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L70-L75)
---
## home-dir
### clj
_platforms: clj_
```clojure
(home-dir)
```
Returns the user’s home directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L77-L79)
### cljs
_platforms: cljs_
```clojure
(home-dir)
```
Returns the user’s home directory.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L77-L79)
---
## audio-dir
### clj
_platforms: clj_
```clojure
(audio-dir)
```
Returns the user’s audio or music directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L81-L83)
### cljs
_platforms: cljs_
```clojure
(audio-dir)
```
Returns the user’s audio or music directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L81-L83)
---
## desktop-dir
### clj
_platforms: clj_
```clojure
(desktop-dir)
```
Returns the user’s desktop directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L85-L87)
### cljs
_platforms: cljs_
```clojure
(desktop-dir)
```
Returns the user’s desktop directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L85-L87)
---
## document-dir
### clj
_platforms: clj_
```clojure
(document-dir)
```
Returns the user’s documents directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L89-L91)
### cljs
_platforms: cljs_
```clojure
(document-dir)
```
Returns the user’s documents directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L89-L91)
---
## download-dir
### clj
_platforms: clj_
```clojure
(download-dir)
```
Returns the user’s downloads directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L93-L95)
### cljs
_platforms: cljs_
```clojure
(download-dir)
```
Returns the user’s downloads directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L93-L95)
---
## font-dir
### clj
_platforms: clj_
```clojure
(font-dir)
```
Returns the user’s font directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L97-L99)
### cljs
_platforms: cljs_
```clojure
(font-dir)
```
Returns the user’s font directory, or `nil` when unsupported.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L97-L99)
---
## picture-dir
### clj
_platforms: clj_
```clojure
(picture-dir)
```
Returns the user’s pictures directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L101-L103)
### cljs
_platforms: cljs_
```clojure
(picture-dir)
```
Returns the user’s pictures directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L101-L103)
---
## public-dir
### clj
_platforms: clj_
```clojure
(public-dir)
```
Returns the user’s public sharing directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L105-L107)
### cljs
_platforms: cljs_
```clojure
(public-dir)
```
Returns the user’s public sharing directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L105-L107)
---
## template-dir
### clj
_platforms: clj_
```clojure
(template-dir)
```
Returns the user’s templates directory, or `nil` when unsupported or unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L109-L111)
### cljs
_platforms: cljs_
```clojure
(template-dir)
```
Returns the user’s templates directory, or `nil` when unsupported or unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L109-L111)
---
## video-dir
### clj
_platforms: clj_
```clojure
(video-dir)
```
Returns the user’s videos directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L113-L115)
### cljs
_platforms: cljs_
```clojure
(video-dir)
```
Returns the user’s videos directory, or `nil` when unresolved.
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L113-L115)
---
## state-dir
### clj
_platforms: clj_
```clojure
(state-dir)
(state-dir application)
(state-dir qualifier organization application)
```
Alias of [`state-home`](#state-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L117-L122)
### cljs
_platforms: cljs_
```clojure
(state-dir)
(state-dir application)
(state-dir qualifier organization application)
```
Alias of [`state-home`](#state-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L117-L122)
---
## config-dir
### clj
_platforms: clj_
```clojure
(config-dir)
(config-dir application)
(config-dir qualifier organization application)
```
Alias of [`config-home`](#config-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L124-L129)
### cljs
_platforms: cljs_
```clojure
(config-dir)
(config-dir application)
(config-dir qualifier organization application)
```
Alias of [`config-home`](#config-home).
[source,window=_blank](https://github.com/outskirtslabs/dirs/blob/main/src/ol/dirs.cljc#L124-L129)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning).
## ++[++UNRELEASED++]++
## `v0.1.0` (2026-03-12)
This is the first public release of this codebase under the `ol.dirs` name.
This release establishes the first published cross-platform directory discovery API for Clojure, ClojureScript on Node, and ClojureDart.
Private variations of the clj/JVM code has been used in production for a awhile, but nonetheless I want to be cautious so we’re starting with sub-1.0 versioning.
## ol.dirs
# ol.dirs
> A 0-dependency implementation of XDG base directories, systemd unit directories, Windows Known Folder paths, and macOS standard directories for Clojure, ClojureScript, and ClojureDart.


[(https://img.shields.io/clojars/v/com.outskirtslabs/dirs.svg)]
`ol.dirs` returns string paths or vectors of string paths. It does not create directories, does not check whether they exist, and carries no runtime dependencies.
## Support Matrix
| Runtime | Linux | macOS | Windows |
| --- | --- | --- | --- |
| Clojure | yes | yes | yes |
| ClojureScript on Node | yes | yes | yes |
| ClojureDart | yes | yes | yes |
Project status: **[Maturing](https://docs.outskirtslabs.com/open-source-vital-signs#maturing)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/dirs {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/dirs "0.1.0"]
```
## Quick Start
```clojure
(require '[ol.dirs :as dirs])
(dirs/config-home)
;; => "/home/alice/.config"
(dirs/config-home "My App")
;; => "/home/alice/.config/my-app"
(dirs/config-home "org" "Acme Corp" "My App")
;; => Linux: "/home/alice/.config/my-app"
;; => macOS: "/Users/Alice/Library/Application Support/org.Acme-Corp.My-App"
;; => Windows: "C:\\Users\\Alice\\AppData\\Roaming\\Acme Corp\\My App"
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.dirs/next/)
* [API Reference](https://docs.outskirtslabs.com/ol.dirs/next/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/dirs/issues)
## API Notes
These functions support app-specific arities. Arity `0` returns the base directory, arity `1` appends an application name, and arity `3` appends a platform-specific qualifier, organization, and application path. The concrete path shapes are shown below in the `App Path Rules` table and in the example above.
`data-home`, `config-home`, `state-home`, `cache-home`, `runtime-dir`, `executable-dir`, `preference-dir`, `state-dir`, and `config-dir` all support those same arities.
`data-dirs` and `config-dirs` do too, but return vectors and append the application path to each search root.
### App Path Rules
| Form | Linux | macOS | Windows |
| --- | --- | --- | --- |
| `("My App")` | `my-app` | `My-App` | `My App` |
| `("org" "Acme Corp" "My App")` | `my-app` | `org.Acme-Corp.My-App` | `Acme Corp\My App` |
### Base Directories
| Function | Linux | macOS | Windows |
| --- | --- | --- | --- |
| `home-dir` | `$HOME` | `$HOME` | `%USERPROFILE%` |
| `data-home` | `$XDG_DATA_HOME` or `$HOME/.local/share` | `$HOME/Library/Application Support` | `%AppData%` |
| `config-home` | `$XDG_CONFIG_HOME` or `$HOME/.config` | `$HOME/Library/Application Support` | `%AppData%` |
| `state-home` | `$XDG_STATE_HOME` or `$HOME/.local/state` | `$HOME/Library/Application Support` | `%LocalAppData%` |
| `cache-home` | `$XDG_CACHE_HOME` or `$HOME/.cache` | `$HOME/Library/Caches` | `%LocalAppData%` |
| `data-dirs` | `$XDG_DATA_DIRS` or `/usr/local/share`, `/usr/share` | `/Library/Application Support` | `%ProgramData%` |
| `config-dirs` | `$XDG_CONFIG_DIRS` or `/etc/xdg` | `/Library/Application Support` | `%ProgramData%` |
| `preference-dir` | same as `config-home` | `$HOME/Library/Preferences` | `%AppData%` |
| `runtime-dir` | `$XDG_RUNTIME_DIR` or `nil` | `nil` | `nil` |
| `executable-dir` | `$XDG_BIN_HOME` or `$XDG_DATA_HOME/../bin` | `nil` | `nil` |
On Linux, trusted systemd units can override `config-home`, `state-home`, and `cache-home` via `CONFIGURATION_DIRECTORY`, `STATE_DIRECTORY`, and `CACHE_DIRECTORY` as documented in [`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html).
### User-Facing Directories
| Function | Linux | macOS | Windows |
| --- | --- | --- | --- |
| `audio-dir` | `XDG_MUSIC_DIR` from `user-dirs.dirs` | `$HOME/Music` | Music Known Folder |
| `desktop-dir` | `XDG_DESKTOP_DIR` from `user-dirs.dirs` | `$HOME/Desktop` | Desktop Known Folder |
| `document-dir` | `XDG_DOCUMENTS_DIR` from `user-dirs.dirs` | `$HOME/Documents` | Documents Known Folder |
| `download-dir` | `XDG_DOWNLOAD_DIR` from `user-dirs.dirs` | `$HOME/Downloads` | Downloads Known Folder |
| `font-dir` | `$XDG_DATA_HOME/fonts` | `$HOME/Library/Fonts` | `nil` |
| `picture-dir` | `XDG_PICTURES_DIR` from `user-dirs.dirs` | `$HOME/Pictures` | Pictures Known Folder |
| `public-dir` | `XDG_PUBLICSHARE_DIR` from `user-dirs.dirs` | `$HOME/Public` | Public Known Folder |
| `template-dir` | `XDG_TEMPLATES_DIR` from `user-dirs.dirs` | `nil` | Templates Known Folder |
| `video-dir` | `XDG_VIDEOS_DIR` from `user-dirs.dirs` | `$HOME/Movies` | Videos Known Folder |
## Nil Semantics
These functions may return `nil` by design:
* `runtime-dir` and `executable-dir` on non-Linux platforms
* `font-dir` on Windows
* `template-dir` on macOS
* Linux user-facing directories when `user-dirs.dirs` is missing, unreadable, disabled, or malformed
## Development
```bash
bb test:clj
bb test:cljs
bb test:cljd
bb test
```
The CLJD workspace commands live under [`dart/`](dart/), because the Dart toolchain emits generated files that are easier to keep isolated there.
## License
Copyright (C) 2026 Casey Link
Distributed under the [EUPL-1.2](https://spdx.org/licenses/EUPL-1.2.html).
## Security policy
# Security policy
## Advisories
All security advisories for `ol.dirs` will be posted [on GitHub](https://github.com/outskirtslabs/dirs/security/advisories).
## Reporting a vulnerability
Please report possible security vulnerabilities [via GitHub](https://github.com/outskirtslabs/dirs/security/advisories), or by emailing me at `casey@outskirtslabs.com`. You may encrypt email with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
[Casey Link](https://casey.link)
## ol.sfv.impl
# ol.sfv.impl
## token
```clojure
(token s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L15-L15)
---
## token?
```clojure
(token? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L16-L16)
---
## decimal
```clojure
(decimal x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L18-L18)
---
## decimal?
```clojure
(decimal? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L19-L19)
---
## integer
```clojure
(integer n)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L21-L21)
---
## integer?
```clojure
(integer? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L22-L22)
---
## string
```clojure
(string s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L24-L24)
---
## string?
```clojure
(string? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L25-L25)
---
## dstring
```clojure
(dstring s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L27-L27)
---
## dstring?
```clojure
(dstring? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L28-L28)
---
## bytes
```clojure
(bytes b)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L30-L30)
---
## bytes?
```clojure
(bytes? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L31-L31)
---
## bool
```clojure
(bool b)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L33-L33)
---
## bool?
```clojure
(bool? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L34-L34)
---
## date
```clojure
(date secs)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L36-L36)
---
## date?
```clojure
(date? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L37-L37)
---
## params
```clojure
(params & kvs)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L40-L47)
---
## param-get
```clojure
(param-get ps k)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L49-L49)
---
## param-keys
```clojure
(param-keys ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L50-L50)
---
## item
```clojure
(item bare)
(item bare ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L53-L55)
---
## item?
```clojure
(item? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L56-L56)
---
## item-bare
```clojure
(item-bare i)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L58-L58)
---
## item-params
```clojure
(item-params i)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L59-L59)
---
## inner-list
```clojure
(inner-list items)
(inner-list items ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L61-L63)
---
## inner-list?
```clojure
(inner-list? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L65-L65)
---
## inner-items
```clojure
(inner-items il)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L66-L66)
---
## inner-params
```clojure
(inner-params il)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L67-L67)
---
## sf-list
```clojure
(sf-list members)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L69-L69)
---
## sf-list?
```clojure
(sf-list? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L71-L71)
---
## list-members
```clojure
(list-members l)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L72-L72)
---
## sf-dict
```clojure
(sf-dict entries)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L73-L73)
---
## sf-dict?
```clojure
(sf-dict? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L75-L75)
---
## dict-keys
```clojure
(dict-keys d)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L76-L76)
---
## dict-get
```clojure
(dict-get d k)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L77-L77)
---
## dict->pairs
```clojure
(dict->pairs d)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L78-L78)
---
## flag
```clojure
(flag)
(flag ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L80-L82)
---
## flag?
```clojure
(flag? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L84-L84)
---
## ascii-string
```clojure
(ascii-string s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L89-L98)
---
## validate-field-line
```clojure
(validate-field-line s)
```
Validate field line characters and return position of first invalid character, or nil if valid
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L100-L109)
---
## init-ctx
```clojure
(init-ctx s-or-bytes)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L111-L113)
---
## eof?
```clojure
(eof? ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L115-L115)
---
## peek-char
```clojure
(peek-char ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L116-L118)
---
## consume-char
```clojure
(consume-char ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L120-L121)
---
## skip-ows
```clojure
(skip-ows ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L123-L126)
---
## skip-sp
```clojure
(skip-sp ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L128-L131)
---
## digit?
```clojure
(digit? ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L136-L137)
---
## alpha?
```clojure
(alpha? ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L139-L141)
---
## lcalpha?
```clojure
(lcalpha? ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L143-L144)
---
## tchar?
```clojure
(tchar? ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L148-L148)
---
## token-char?
```clojure
(token-char? ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L149-L149)
---
## lc-hexdig?
```clojure
(lc-hexdig? ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L152-L154)
---
## hex-digit-value
```clojure
(hex-digit-value ch)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L156-L160)
---
## parse-error
```clojure
(parse-error ctx reason & {:keys [found expected]})
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L162-L165)
---
## parse-key
```clojure
(parse-key ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L170-L187)
---
## parse-integer-or-decimal
```clojure
(parse-integer-or-decimal ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L193-L246)
---
## parse-string
```clojure
(parse-string ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L248-L277)
---
## parse-token
```clojure
(parse-token ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L278-L300)
---
## parse-byte-sequence
```clojure
(parse-byte-sequence ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L301-L332)
---
## parse-sfv-boolean
```clojure
(parse-sfv-boolean ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L334-L354)
---
## parse-date
```clojure
(parse-date ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L356-L374)
---
## decode-percent-sequence
```clojure
(decode-percent-sequence ctx)
```
Decode a percent-encoded sequence (%XX) into a byte value.
Returns [byte new-ctx] or throws parse error.
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L376-L389)
---
## parse-display-string
```clojure
(parse-display-string ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L391-L498)
---
## parse-bare-item
```clojure
(parse-bare-item ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L500-L534)
---
## parse-parameters
```clojure
(parse-parameters ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L536-L568)
---
## parse-item
```clojure
(parse-item ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L569-L576)
---
## parse-inner-list
```clojure
(parse-inner-list ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L578-L602)
---
## parse-item-or-inner-list
```clojure
(parse-item-or-inner-list ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L604-L611)
---
## parse-list-members
```clojure
(parse-list-members ctx)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L613-L632)
---
## parse-list
```clojure
(parse-list s-or-bytes)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L634-L642)
---
## parse-dict
```clojure
(parse-dict s-or-bytes)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L644-L693)
---
## parse
```clojure
(parse field-type s-or-bytes)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L695-L720)
---
## serialize-integer
```clojure
(serialize-integer v)
```
RFC 9651 §4.1.4: Serializing an Integer
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L725-L731)
---
## serialize-decimal
```clojure
(serialize-decimal v)
```
RFC 9651 §4.1.5: Serializing a Decimal
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L733-L754)
---
## serialize-string
```clojure
(serialize-string v)
```
RFC 9651 §4.1.6: Serializing a String
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L756-L769)
---
## serialize-token
```clojure
(serialize-token v)
```
RFC 9651 §4.1.7: Serializing a Token
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L771-L783)
---
## serialize-byte-sequence
```clojure
(serialize-byte-sequence v)
```
RFC 9651 §4.1.8: Serializing a Byte Sequence
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L785-L790)
---
## serialize-boolean
```clojure
(serialize-boolean v)
```
RFC 9651 §4.1.9: Serializing a Boolean
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L792-L795)
---
## serialize-date
```clojure
(serialize-date v)
```
RFC 9651 §4.1.10: Serializing a Date
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L797-L803)
---
## serialize-display-string
```clojure
(serialize-display-string v)
```
RFC 9651 §4.1.11: Serializing a Display String
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L805-L820)
---
## serialize-bare-item
```clojure
(serialize-bare-item item)
```
RFC 9651 §4.1.3.1: Serializing a Bare Item
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L822-L835)
---
## serialize-key
```clojure
(serialize-key k)
```
RFC 9651 §4.1.1.3: Serializing a Key
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L837-L850)
---
## serialize-parameters
```clojure
(serialize-parameters params)
```
RFC 9651 §4.1.1.2: Serializing Parameters
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L852-L863)
---
## serialize-item
```clojure
(serialize-item item)
```
RFC 9651 §4.1.3: Serializing an Item
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L865-L871)
---
## serialize-inner-list
```clojure
(serialize-inner-list inner-list)
```
RFC 9651 §4.1.1.1: Serializing an Inner List
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L873-L888)
---
## serialize-list
```clojure
(serialize-list lst)
```
RFC 9651 §4.1.1: Serializing a List
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L890-L906)
---
## serialize-dict
```clojure
(serialize-dict dict)
```
RFC 9651 §4.1.2: Serializing a Dictionary
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L908-L934)
---
## serialize
```clojure
(serialize x)
```
RFC 9651 §4.1: Serializing Structured Fields
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L936-L943)
---
## combine-field-lines
```clojure
(combine-field-lines lines)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv/impl.clj#L945-L945)
## ol.sfv
# ol.sfv
RFC 9651 Structured Field Values for HTTP.
See the project website for info on motivation and design:
<https://github.com/outskirtslabs/sfv>
This ns provides parsing and serialization of Structured Fields containing
Items, Lists, and Dictionaries with Parameters. Returns precise AST
representations that round-trip byte-for-byte with RFC 9651 strings.
Primary functions: [`parse`](#parse), [`parse-item`](#parse-item), [`parse-list`](#parse-list), [`parse-dict`](#parse-dict),
[`serialize`](#serialize)
## Conventions
* `s-or-bytes`: Input string or byte array (decoded as ascii) containing HTTP field value
* `field-type`: One of `:item`, `:list`, or `:dict` specifying the top-level structure
* `:bare`: The raw value within an Item (Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String)
## Primitive Types
Each Item’s `:bare` is one of these RFC 9651 types:
| SFV type | Header | AST example | Clojure type (`:value`) |
|----------------|--------------------------------|--------------------------------------------|---------------------------------------|
| Integer | `42`, `-17`, `999999999999999` | `{:type :integer :value 1618884473}` | `long` |
| Decimal | `3.14`, `-0.5` | `{:type :decimal :value 3.14M}` | `BigDecimal` |
| String | `" hello world "` | `{:type :string :value " hello "}` | `java.lang.String` |
| Token | `simple-token` | `{:type :token :value " simple-token "}` | `String` |
| Byte Sequence | `:SGVsbG8=:` | `{:type :bytes :value }` | `byte[]` |
| Boolean | `?1` / `?0` | `{:type :boolean :value true}` | `true` / `false` |
| Date | `@1659578233` | `{:type :date :value 1659578233}` | epoch seconds as `long` |
| Display String | `%" Gr%c3%bc%c3%9fe "` | `{:type :display :value " Grüße "}` | `String` (percent-decoded, validated) |
## parse
```clojure
(parse field-type s-or-bytes)
```
Parse a Structured Field of the given `field-type`.
Takes a `field-type` (`:list`, `:dict`, or `:item`) and a string or byte array.
Returns an AST representation following RFC 9651.
```clojure
(parse :item "42")
;; => {:type :item :bare {:type :integer :value 42} :params []}
(parse :dict "max-age=3600, must-revalidate")
;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L39-L53)
---
## parse-list
```clojure
(parse-list s-or-bytes)
```
Parse a Structured Field List from `s-or-bytes`.
Returns a List AST with `:type :list` and `:members` vector containing Items and Inner Lists.
```clojure
(parse-list "apple, pear;sweet=true, orange")
;; => {:type :list :members [...]}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L55-L65)
---
## parse-dict
```clojure
(parse-dict s-or-bytes)
```
Parse a Structured Field Dictionary from `s-or-bytes`.
Returns a Dictionary AST with `:type :dict` and `:entries` as ordered key-value pairs.
Each entry maps from a key to either an Item or Inner List.
```clojure
(parse-dict "max-age=3600, must-revalidate")
;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L67-L78)
---
## parse-item
```clojure
(parse-item s-or-bytes)
```
Parse a Structured Field Item from `s-or-bytes`.
Returns an Item AST with `:type :item`, `:bare` value, and `:params` Parameters.
The bare value can be Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String.
```clojure
(parse-item "42")
;; => {:type :item :bare {:type :integer :value 42} :params []}
(parse-item "pear;sweet")
;; => {:type :item :bare {:type :token :value "pear"} :params `"sweet" {...}`}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L80-L94)
---
## serialize
```clojure
(serialize x)
```
Serialize a Structured Field AST `x` to its string representation.
Takes any parsed AST (Item, List, or Dictionary) and returns the RFC 9651 string.
See [`serialize-item`](api/ol-sfv-impl.adoc#serialize-item), [`serialize-list`](api/ol-sfv-impl.adoc#serialize-list), and [`serialize-dict`](api/ol-sfv-impl.adoc#serialize-dict) for type-specific variants.
```clojure
(serialize {:type :item :bare {:type :integer :value 42} :params []})
;; => "42"
(serialize-list {:type :list :members [...]})
;; => "apple, pear;sweet=true, orange"
(serialize-dict {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]})
;; => "max-age=3600, must-revalidate"
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L96-L113)
---
## token
```clojure
(token s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L116-L117)
---
## token?
```clojure
(token? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L119-L120)
---
## decimal
```clojure
(decimal x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L122-L123)
---
## decimal?
```clojure
(decimal? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L124-L125)
---
## integer
```clojure
(integer n)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L127-L128)
---
## integer?
```clojure
(integer? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L130-L131)
---
## string
```clojure
(string s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L133-L134)
---
## string?
```clojure
(string? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L136-L137)
---
## dstring
```clojure
(dstring s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L139-L140)
---
## dstring?
```clojure
(dstring? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L142-L143)
---
## bytes
```clojure
(bytes b)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L145-L146)
---
## bytes?
```clojure
(bytes? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L148-L149)
---
## bool
```clojure
(bool b)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L151-L152)
---
## bool?
```clojure
(bool? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L154-L155)
---
## date
```clojure
(date seconds)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L157-L158)
---
## date?
```clojure
(date? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L160-L161)
---
## params
```clojure
(params & kvs)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L164-L165)
---
## param-get
```clojure
(param-get ps k)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L167-L168)
---
## param-keys
```clojure
(param-keys ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L170-L171)
---
## item
```clojure
(item bare)
(item bare ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L174-L178)
---
## item?
```clojure
(item? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L180-L181)
---
## item-bare
```clojure
(item-bare i)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L183-L184)
---
## item-params
```clojure
(item-params i)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L186-L187)
---
## inner-list
```clojure
(inner-list items)
(inner-list items ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L190-L194)
---
## inner-list?
```clojure
(inner-list? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L196-L197)
---
## inner-items
```clojure
(inner-items il)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L199-L200)
---
## inner-params
```clojure
(inner-params il)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L202-L203)
---
## sf-list
```clojure
(sf-list members)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L206-L207)
---
## sf-list?
```clojure
(sf-list? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L209-L210)
---
## list-members
```clojure
(list-members l)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L212-L213)
---
## sf-dict
```clojure
(sf-dict entries)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L216-L217)
---
## sf-dict?
```clojure
(sf-dict? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L219-L220)
---
## dict-keys
```clojure
(dict-keys d)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L222-L223)
---
## dict-get
```clojure
(dict-get d k)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L225-L226)
---
## dict->pairs
```clojure
(dict->pairs d)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L228-L229)
---
## flag
```clojure
(flag)
(flag ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L232-L236)
---
## flag?
```clojure
(flag? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/v0.1.x/src/ol/sfv.clj#L238-L239)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## ++[++UNRELEASED++]++
* Removed references in docstrings to old functions that existed in an
unreleased version
* Reduced stdout during test suite execution
## `v0.1.0` (2025-09-08)
This is the first public release of this codebase under the `ol.sfv`
name.
Private variations of this code has been used in production for a
awhile, but nonetheless I want to be cautious so we’re starting with
sub-1.0 versioning.
Please report any problems and let me know if anything is unclear,
inconvenient, etc. Thank you! 🙏
## ol.sfv
# ol.sfv
> A 0-dependency Clojure library for parsing and generating Structured
> Field Values for HTTP (RFC 9651/8941)
https://github.com/outskirtslabs/sfv/actions[image:https://github.com/outskirtslabs/sfv/actions/workflows/ci.yml/badge.svg[Build
Status]]


https://clojars.org/com.outskirtslabs/sfv[image:https://img.shields.io/clojars/v/com.outskirtslabs/sfv.svg[Clojars
Project]]
Structured Field Values (SFV) as defined in
[RFC 9651](https://datatracker.ietf.org/doc/html/rfc9651) provide a
standardized way to encode complex data structures in HTTP headers.
`ol.sfv` library implements the specification, providing parsing and
serialization capabilities.
This is a low-level library that emits and consumes the RFC 9651 AST,
which unfortunately does not map cleanly to Clojure datastructures. For
practical use, it should be wrapped in higher-level functions or
libraries that implement specific HTTP headers like
`Permissions-Policy`, `Signature-Input`, or `Signature` headers (any any
future HTTP headers).
Key features:
* Complete RFC 9651 implementation with AST-level parsing and
serialization with zero deps
* Extensively tested (`2853 tests, 3220 assertions`)
* Precise round-trip fidelity - parse and serialize back to identical
strings
* Validation and error reporting
* JVM/Graal target
Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**.
## Installation
```clojure
{:deps {com.outskirtslabs/sfv {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/sfv "0.1.0"]
```
## Quick Start
```clojure
(ns myapp.core
(:require [ol.sfv :as sfv]))
;; Integer
(sfv/parse-item "42")
{:type :item :bare {:type :integer :value 42} :params []}
;; They are round trippable
(sfv/serialize-item {:type :item :bare {:type :integer :value 42}})
;; => "42"
;; Display String
(sfv/parse-item "%\"Gr%c3%bc%c3%9fe\"")
{:type :item :bare {:type :dstring :value "Grüße"} :params []}
(sfv/serialize-item {:type :item :bare {:type :dstring :value "السلام عليكم"} :params []})
"\"%d8%a7%d9%84%d8%b3%d9%84%d8%a7%d9%85 %d8%b9%d9%84%d9%8a%d9%83%d9%85\""
;; Dates
(sfv/parse-item "@1659578233")
{:type :item, :bare {:type :date, :value 1659578233}, :params []}
;; Items can have params attached
(sfv/parse-item "pear;sweet=?1")
{:type :item
:bare {:type :token :value "pear"}
:params [["sweet" {:type :token :value true}]]}
;; Parse a list with parameters
(sfv/parse-list "apple, pear;sweet=true, orange")
{:type :list
:members [{:type :item :bare {:type :token :value "apple"} :params []}
{:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value "true"}]]}
{:type :item :bare {:type :token :value "orange"} :params []}]}
;; Dictionaries
(sfv/parse-dict "max-age=3600, must-revalidate")
{:type :dict
:entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}]
["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]}
;; Dictionaries with inner lists and params
(sfv/parse-dict "trees=(\"spruce\";type=conifer \"oak\";type=deciduous)")
{:type :dict
:entries [["trees"
{:type :inner-list
:items [{:type :item :bare {:type :string :value "spruce"}
:params [["type" {:type :token :value "conifer"}]]}
{:type :item :bare {:type :string :value "oak"}
:params [["type" {:type :token :value "deciduous"}]]}]
:params []}]]}
(sfv/parse-dict "foods=(\"burger\";sandwich=?1 \"pizza\";sandwich=?0 \"hot dog\";sandwich=?1);comprehensive=?0")
{:type :dict
:entries [["foods" {:type :inner-list
:items [{:type :item :bare {:type :string :value "burger"}
:params [["sandwich" {:type :boolean :value true}]]}
{:type :item :bare {:type :string :value "pizza"}
:params [["sandwich" {:type :boolean :value false}]]}
{:type :item :bare {:type :string :value "hot dog"}
:params [["sandwich" {:type :boolean :value true}]]}]
:params [["comprehensive" {:type :boolean :value false}]]}]]}
;; List with Items and Inner List
(sfv/parse-list "circle;color=red, square;filled=?0, (triangle;size=3 rectangle;size=4)")
{:type :list
:members [{:type :item :bare {:type :token :value "circle"}
:params [["color" {:type :token :value "red"}]]}
{:type :item :bare {:type :token :value "square"}
:params [["filled" {:type :boolean :value false}]]}
{:type :inner-list :items [{:type :item :bare {:type :token :value "triangle"}
:params [["size" {:type :integer :value 3}]]}
{:type :item :bare {:type :token :value "rectangle"}
:params [["size" {:type :integer :value 4}]]}]
:params []}]}
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.sfv/0.1/)
* [API Reference](https://docs.outskirtslabs.com/ol.sfv/0.1/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/sfv/issues)
## Data Types
`ol.sfv` is a low-level, AST-oriented implementation of RFC 9651. We
don’t try to coerce values into nice
Clojure shapes; instead we
expose a precise tree that round-trips byte-for-byte. This is important
because Structured Fields carry ordering information that ordinary
Clojure maps can’t reliably preserve across platforms and sizes.
### Primitive types
RFC 9651 defines several primitive types, all supported.
Each Item’s `:bare` is one of the following:
| | | | |
| --- | --- | --- | --- |
| SFV type | Header | AST example | Clojure type (`:value`) |
| Integer | `42`, `-17`, `999999999999999` | `++{++:type :integer :value 1618884473}` | `long` |
| Decimal | `3.14`, `-0.5` | `++{++:type :decimal :value 3.14M}` | `BigDecimal` |
| String | `"hello world"` | `++{++:type :string :value "hello"}` | `java.lang.String` |
| Token | `simple-token` | `++{++:type :token :value "simple-token"}` | `String` |
| Byte Sequence | `:SGVsbG8=:` | `+{++:type :bytes :value ++<++platform bytes++>++}` | `byte++[]+` |
| Boolean | `?1` / `?0` | `++{++:type :boolean :value true}` | `true` / `false` |
| Date | `@1659578233` | `++{++:type :date :value 1659578233}` | epoch seconds as `long` |
| Display String | `%"Gr%c3%bc%c3%9fe"` | `++{++:type :display :value "Grüße"}` | `String` (percent-decoded, validated) |
* Decimals obey SFV’s constraints (≤3 fractional digits, length limits)
and are parsed to `BigDecimal` to avoid float rounding.
* Byte sequences are base64 inside the header; we give you the decoded
bytes.
### Container types
* **Item** — a **bare value** plus optional **parameters**
```clojure
{:type :item
:bare
:params [ [param-name ] ... ]}
```
* **List** — a sequence of **items** or **inner lists**
```clojure
{:type :list
:members [ ... ]}
```
* **Dictionary** — an **ordered** sequence of key→member entries
```clojure
{:type :dict
:entries [ [key ] ... ]}
```
* **Inner List** — a parenthesized list (appears inside List/Dictionary)
with its own parameters
```clojure
{:type :inner-list
:items [ - ... ]
:params [ [param-name ] ... ]}
```
Keys are parsed as lower-case identifiers per the spec.
### Why not plain Clojure maps?
Dictionaries and parameter lists in SFV have a defined member order.
That order may or may not be semantically meaningful, that depends on
the specific header.
To make ordering explicit and stable, we represent dictionaries and
parameter lists as vectors of ++[++k v++]++ pairs. That’s portable,
preserves order, and lets you implement whatever key semantics you need
at a higher level.
### Parameters
Parameters attach metadata to an Item or an Inner List:
* Syntax: `;key++[++=value++]++` repeating after the base value If
`=value` is omitted, the parameter value is the boolean `true`.
* In the AST, parameters are always a vector of `+[++name value++]+`
pairs in the order seen:
```clojure
(sfv/parse-item "pear;sweet=?1")
{:type :item
:bare {:type :token :value "pear"}
:params [["sweet" {:type :boolean :value true}]]}
```
* Parameter values are bare values (not nested items). You’ll see the
same `:type`/`:value` shapes as the table above
* Ordering is preserved for round-trip fidelity
### Dictionaries
Dictionaries map from a key to either an Item or an Inner List. We keep
them as an ordered vector of entries:
```clojure
(sfv/parse-dict "max-age=3600, must-revalidate")
{:type :dict
:entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}]
["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]}
```
## Building Header-Specific Libraries
This library is designed to be wrapped by more specific implementations:
```clojure
(ns myapp.cache-control
(:require [ol.sfv :as sfv]))
(defn parse-cache-control [header-value]
(let [parsed (sfv/parse-dict header-value)]
(reduce (fn [acc [key item]]
(let [value (get-in item [:bare :value])]
(assoc acc (keyword key) value)))
{}
(:entries parsed))))
(parse-cache-control "max-age=3600, must-revalidate")
;; => {:max-age 3600, :must-revalidate true}
```
## Recommended Reading
Structured Field Values provide a robust foundation for modern HTTP
header design:
* https://datatracker.ietf.org/doc/html/rfc9651[RFC 9651: Structured
Field Values for HTTP] - The complete specification
* https://datatracker.ietf.org/doc/html/rfc9421[RFC 9421: HTTP Message
Signatures] - A major consumer of structured fields
## Security
See [here](https://github.com/outskirtslabs/sfv/security) for security
advisories or to report a security vulnerability.
## License
Copyright © 2025 Casey Link casey@outskirtslabs.com
Distributed under the [MIT License](./LICENSE)
## ol.sfv
# ol.sfv
RFC 9651 Structured Field Values for HTTP.
See the project website for info on motivation and design:
<https://github.com/outskirtslabs/sfv>
This ns provides parsing and serialization of Structured Fields containing
Items, Lists, and Dictionaries with Parameters. Returns precise AST
representations that round-trip byte-for-byte with RFC 9651 strings.
Primary functions: [`parse`](#parse), [`parse-item`](#parse-item), [`parse-list`](#parse-list), [`parse-dict`](#parse-dict),
[`serialize`](#serialize)
## Conventions
* `s-or-bytes`: Input string or byte array (decoded as ascii) containing HTTP field value
* `field-type`: One of `:item`, `:list`, or `:dict` specifying the top-level structure
* `:bare`: The raw value within an Item (Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String)
## Primitive Types
Each Item’s `:bare` is one of these RFC 9651 types:
| SFV type | Header | AST example | Clojure type (`:value`) |
|----------------|--------------------------------|--------------------------------------------|---------------------------------------|
| Integer | `42`, `-17`, `999999999999999` | `{:type :integer :value 1618884473}` | `long` |
| Decimal | `3.14`, `-0.5` | `{:type :decimal :value 3.14M}` | `BigDecimal` |
| String | `" hello world "` | `{:type :string :value " hello "}` | `java.lang.String` |
| Token | `simple-token` | `{:type :token :value " simple-token "}` | `String` |
| Byte Sequence | `:SGVsbG8=:` | `{:type :bytes :value }` | `byte[]` |
| Boolean | `?1` / `?0` | `{:type :boolean :value true}` | `true` / `false` |
| Date | `@1659578233` | `{:type :date :value 1659578233}` | epoch seconds as `long` |
| Display String | `%" Gr%c3%bc%c3%9fe "` | `{:type :display :value " Grüße "}` | `String` (percent-decoded, validated) |
## parse
```clojure
(parse field-type s-or-bytes)
```
Parse a Structured Field of the given `field-type`.
Takes a `field-type` (`:list`, `:dict`, or `:item`) and a string or byte array.
Returns an AST representation following RFC 9651.
```clojure
(parse :item "42")
;; => {:type :item :bare {:type :integer :value 42} :params []}
(parse :dict "max-age=3600, must-revalidate")
;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L41-L55)
---
## parse-list
```clojure
(parse-list s-or-bytes)
```
Parse a Structured Field List from `s-or-bytes`.
Returns a List AST with `:type :list` and `:members` vector containing Items and Inner Lists.
```clojure
(parse-list "apple, pear;sweet=true, orange")
;; => {:type :list :members [...]}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L57-L67)
---
## parse-dict
```clojure
(parse-dict s-or-bytes)
```
Parse a Structured Field Dictionary from `s-or-bytes`.
Returns a Dictionary AST with `:type :dict` and `:entries` as ordered key-value pairs.
Each entry maps from a key to either an Item or Inner List.
```clojure
(parse-dict "max-age=3600, must-revalidate")
;; => {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L69-L80)
---
## parse-item
```clojure
(parse-item s-or-bytes)
```
Parse a Structured Field Item from `s-or-bytes`.
Returns an Item AST with `:type :item`, `:bare` value, and `:params` Parameters.
The bare value can be Integer, Decimal, String, Token, Byte Sequence, Boolean, Date, or Display String.
```clojure
(parse-item "42")
;; => {:type :item :bare {:type :integer :value 42} :params []}
(parse-item "pear;sweet")
;; => {:type :item :bare {:type :token :value "pear"} :params `"sweet" {...}`}
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L82-L96)
---
## serialize
```clojure
(serialize x)
```
Serialize a Structured Field AST `x` to its string representation.
Takes any parsed AST (Item, List, or Dictionary) and returns the RFC 9651 string.
```clojure
(serialize {:type :item :bare {:type :integer :value 42} :params []})
;; => "42"
(serialize-list {:type :list :members [...]})
;; => "apple, pear;sweet=true, orange"
(serialize-dict {:type :dict :entries [["max-age" {...}] ["must-revalidate" {...}]]})
;; => "max-age=3600, must-revalidate"
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L98-L114)
---
## token
```clojure
(token s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L117-L118)
---
## token?
```clojure
(token? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L120-L121)
---
## decimal
```clojure
(decimal x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L123-L124)
---
## decimal?
```clojure
(decimal? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L125-L126)
---
## integer
```clojure
(integer n)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L128-L129)
---
## integer?
```clojure
(integer? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L131-L132)
---
## string
```clojure
(string s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L134-L135)
---
## string?
```clojure
(string? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L137-L138)
---
## dstring
```clojure
(dstring s)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L140-L141)
---
## dstring?
```clojure
(dstring? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L143-L144)
---
## bytes
```clojure
(bytes b)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L146-L147)
---
## bytes?
```clojure
(bytes? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L149-L150)
---
## bool
```clojure
(bool b)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L152-L153)
---
## bool?
```clojure
(bool? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L155-L156)
---
## date
```clojure
(date seconds)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L158-L159)
---
## date?
```clojure
(date? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L161-L162)
---
## params
```clojure
(params & kvs)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L165-L166)
---
## param-get
```clojure
(param-get ps k)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L168-L169)
---
## param-keys
```clojure
(param-keys ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L171-L172)
---
## item
```clojure
(item bare)
(item bare ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L175-L179)
---
## item?
```clojure
(item? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L181-L182)
---
## item-bare
```clojure
(item-bare i)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L184-L185)
---
## item-params
```clojure
(item-params i)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L187-L188)
---
## inner-list
```clojure
(inner-list items)
(inner-list items ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L191-L195)
---
## inner-list?
```clojure
(inner-list? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L197-L198)
---
## inner-items
```clojure
(inner-items il)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L200-L201)
---
## inner-params
```clojure
(inner-params il)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L203-L204)
---
## sf-list
```clojure
(sf-list members)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L207-L208)
---
## sf-list?
```clojure
(sf-list? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L210-L211)
---
## list-members
```clojure
(list-members l)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L213-L214)
---
## sf-dict
```clojure
(sf-dict entries)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L217-L218)
---
## sf-dict?
```clojure
(sf-dict? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L220-L221)
---
## dict-keys
```clojure
(dict-keys d)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L223-L224)
---
## dict-get
```clojure
(dict-get d k)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L226-L227)
---
## dict->pairs
```clojure
(dict->pairs d)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L229-L230)
---
## flag
```clojure
(flag)
(flag ps)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L233-L237)
---
## flag?
```clojure
(flag? x)
```
[source,window=_blank](https://github.com/outskirtslabs/sfv/blob/main/src/ol/sfv.clj#L239-L240)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## ++[++UNRELEASED++]++
* Removed references in docstrings to old functions that existed in an
unreleased version
* Reduced stdout during test suite execution
## `v0.1.0` (2025-09-08)
This is the first public release of this codebase under the `ol.sfv`
name.
Private variations of this code has been used in production for a
awhile, but nonetheless I want to be cautious so we’re starting with
sub-1.0 versioning.
Please report any problems and let me know if anything is unclear,
inconvenient, etc. Thank you! 🙏
## ol.sfv
# ol.sfv
> A 0-dependency Clojure library for parsing and generating Structured
> Field Values for HTTP (RFC 9651/8941)



https://clojars.org/com.outskirtslabs/sfv[image:https://img.shields.io/clojars/v/com.outskirtslabs/sfv.svg[Clojars
Project]]
Structured Field Values (SFV) as defined in
[RFC 9651](https://datatracker.ietf.org/doc/html/rfc9651) provide a
standardized way to encode complex data structures in HTTP headers.
`ol.sfv` library implements the specification, providing parsing and
serialization capabilities.
This is a low-level library that emits and consumes the RFC 9651 AST,
which unfortunately does not map cleanly to Clojure datastructures. For
practical use, it should be wrapped in higher-level functions or
libraries that implement specific HTTP headers like
`Permissions-Policy`, `Signature-Input`, or `Signature` headers (any any
future HTTP headers).
Key features:
* Complete RFC 9651 implementation with AST-level parsing and
serialization with zero deps
* Extensively tested (`2853 tests, 3220 assertions`)
* Precise round-trip fidelity - parse and serialize back to identical
strings
* Validation and error reporting
* JVM/Graal target
Project status: **[Stable](https://docs.outskirtslabs.com/open-source-vital-signs#stable)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/sfv {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/sfv "0.1.0"]
```
## Quick Start
```clojure
(ns myapp.core
(:require [ol.sfv :as sfv]))
;; Integer
(sfv/parse-item "42")
{:type :item :bare {:type :integer :value 42} :params []}
;; They are round trippable
(sfv/serialize {:type :item :bare {:type :integer :value 42}})
;; => "42"
;; Display String
(sfv/parse-item "%\"Gr%c3%bc%c3%9fe\"")
{:type :item :bare {:type :dstring :value "Grüße"} :params []}
(sfv/serialize {:type :item :bare {:type :dstring :value "السلام عليكم"} :params []})
"\"%d8%a7%d9%84%d8%b3%d9%84%d8%a7%d9%85 %d8%b9%d9%84%d9%8a%d9%83%d9%85\""
;; Dates
(sfv/parse-item "@1659578233")
{:type :item, :bare {:type :date, :value 1659578233}, :params []}
;; Items can have params attached
(sfv/parse-item "pear;sweet=?1")
{:type :item
:bare {:type :token :value "pear"}
:params [["sweet" {:type :token :value true}]]}
;; Parse a list with parameters
(sfv/parse-list "apple, pear;sweet=true, orange")
{:type :list
:members [{:type :item :bare {:type :token :value "apple"} :params []}
{:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value "true"}]]}
{:type :item :bare {:type :token :value "orange"} :params []}]}
;; Dictionaries
(sfv/parse-dict "max-age=3600, must-revalidate")
{:type :dict
:entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}]
["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]}
;; Dictionaries with inner lists and params
(sfv/parse-dict "trees=(\"spruce\";type=conifer \"oak\";type=deciduous)")
{:type :dict
:entries [["trees"
{:type :inner-list
:items [{:type :item :bare {:type :string :value "spruce"}
:params [["type" {:type :token :value "conifer"}]]}
{:type :item :bare {:type :string :value "oak"}
:params [["type" {:type :token :value "deciduous"}]]}]
:params []}]]}
(sfv/parse-dict "foods=(\"burger\";sandwich=?1 \"pizza\";sandwich=?0 \"hot dog\";sandwich=?1);comprehensive=?0")
{:type :dict
:entries [["foods" {:type :inner-list
:items [{:type :item :bare {:type :string :value "burger"}
:params [["sandwich" {:type :boolean :value true}]]}
{:type :item :bare {:type :string :value "pizza"}
:params [["sandwich" {:type :boolean :value false}]]}
{:type :item :bare {:type :string :value "hot dog"}
:params [["sandwich" {:type :boolean :value true}]]}]
:params [["comprehensive" {:type :boolean :value false}]]}]]}
;; List with Items and Inner List
(sfv/parse-list "circle;color=red, square;filled=?0, (triangle;size=3 rectangle;size=4)")
{:type :list
:members [{:type :item :bare {:type :token :value "circle"}
:params [["color" {:type :token :value "red"}]]}
{:type :item :bare {:type :token :value "square"}
:params [["filled" {:type :boolean :value false}]]}
{:type :inner-list :items [{:type :item :bare {:type :token :value "triangle"}
:params [["size" {:type :integer :value 3}]]}
{:type :item :bare {:type :token :value "rectangle"}
:params [["size" {:type :integer :value 4}]]}]
:params []}]}
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.sfv/0.1/)
* [API Reference](https://docs.outskirtslabs.com/ol.sfv/0.1/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/sfv/issues)
## Data Types
`ol.sfv` is a low-level, AST-oriented implementation of RFC 9651. We
don’t try to coerce values into
nice
Clojure shapes; instead we
expose a precise tree that round-trips byte-for-byte. This is important
because Structured Fields carry ordering information that ordinary
Clojure maps can’t reliably preserve across platforms and sizes.
### Primitive types
RFC 9651 defines several primitive types, all supported.
Each Item’s `:bare` is one of the following:
| | | | |
| --- | --- | --- | --- |
| SFV type | Header | AST example | Clojure type (`:value`) |
| Integer | `42`, `-17`, `999999999999999` | `++{++:type :integer :value 1618884473}` | `long` |
| Decimal | `3.14`, `-0.5` | `++{++:type :decimal :value 3.14M}` | `BigDecimal` |
| String | `"hello world"` | `++{++:type :string :value "hello"}` | `java.lang.String` |
| Token | `simple-token` | `++{++:type :token :value "simple-token"}` | `String` |
| Byte Sequence | `:SGVsbG8=:` | `+{++:type :bytes :value ++<++platform bytes++>++}` | `byte++[]+` |
| Boolean | `?1` / `?0` | `++{++:type :boolean :value true}` | `true` / `false` |
| Date | `@1659578233` | `++{++:type :date :value 1659578233}` | epoch seconds as `long` |
| Display String | `%"Gr%c3%bc%c3%9fe"` | `++{++:type :display :value "Grüße"}` | `String` (percent-decoded, validated) |
* Decimals obey SFV’s constraints (≤3 fractional digits, length limits)
and are parsed to `BigDecimal` to avoid float rounding.
* Byte sequences are base64 inside the header; we give you the decoded
bytes.
### Container types
* **Item** — a **bare value** plus optional **parameters**
```clojure
{:type :item
:bare
:params [ [param-name ] ... ]}
```
* **List** — a sequence of **items** or **inner lists**
```clojure
{:type :list
:members [ ... ]}
```
* **Dictionary** — an **ordered** sequence of key→member entries
```clojure
{:type :dict
:entries [ [key ] ... ]}
```
* **Inner List** — a parenthesized list (appears inside List/Dictionary)
with its own parameters
```clojure
{:type :inner-list
:items [ - ... ]
:params [ [param-name ] ... ]}
```
Keys are parsed as lower-case identifiers per the spec.
### Why not plain Clojure maps?
Dictionaries and parameter lists in SFV have a defined member order.
That order may or may not be semantically meaningful, that depends on
the specific header.
To make ordering explicit and stable, we represent dictionaries and
parameter lists as vectors of ++[++k v++]++ pairs. That’s portable,
preserves order, and lets you implement whatever key semantics you need
at a higher level.
### Parameters
Parameters attach metadata to an Item or an Inner List:
* Syntax: `;key++[++=value++]++` repeating after the base value If
`=value` is omitted, the parameter value is the boolean `true`.
* In the AST, parameters are always a vector of `+[++name value++]+`
pairs in the order seen:
```clojure
(sfv/parse-item "pear;sweet=?1")
{:type :item
:bare {:type :token :value "pear"}
:params [["sweet" {:type :boolean :value true}]]}
```
* Parameter values are bare values (not nested items). You’ll see the
same `:type`/`:value` shapes as the table above
* Ordering is preserved for round-trip fidelity
### Dictionaries
Dictionaries map from a key to either an Item or an Inner List. We keep
them as an ordered vector of entries:
```clojure
(sfv/parse-dict "max-age=3600, must-revalidate")
{:type :dict
:entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}]
["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]}
```
## Building Header-Specific Libraries
This library is designed to be wrapped by more specific implementations:
```clojure
(ns myapp.cache-control
(:require [ol.sfv :as sfv]))
(defn parse-cache-control [header-value]
(let [parsed (sfv/parse-dict header-value)]
(reduce (fn [acc [key item]]
(let [value (get-in item [:bare :value])]
(assoc acc (keyword key) value)))
{}
(:entries parsed))))
(parse-cache-control "max-age=3600, must-revalidate")
;; => {:max-age 3600, :must-revalidate true}
```
## Recommended Reading
Structured Field Values provide a robust foundation for modern HTTP
header design:
* https://datatracker.ietf.org/doc/html/rfc9651[RFC 9651: Structured
Field Values for HTTP] - The complete specification
* https://datatracker.ietf.org/doc/html/rfc9421[RFC 9421: HTTP Message
Signatures] - A major consumer of structured fields
## Security
See [here](https://github.com/outskirtslabs/sfv/security) for security
advisories or to report a security vulnerability.
## License
Copyright © 2025 Casey Link casey@outskirtslabs.com
Distributed under the [MIT License](./LICENSE)
## ol.sops
# ol.sops
## sops
```clojure
(sops cmd)
(sops cmd args-or-opts)
(sops cmd args & opts)
```
A simple wrapper around babashka.process for calling sops.
You can call `sops` like this:
(sops :command ["args vector"] :opt "value" :opt2 "value2")
There are several special opts, that are handled by ol.sops:
:in - An input stream than can be passed to sops
:out - :string if you want to get a string back, nil (the default) will return an output stream you can slurp
:dir - The working directory. Defaults to the current working directory
:sops-path - Path to the sops binary. Defaults to `sops`
:env - a map of environment variables that are passed to the process
All other opts are passed to the `sops` CLI. Consult `sops CMD --help` to see available options.
To pass flags like `--verbose` use `:verbose true`. Always use full names of the options, not the short versions like -v.
Refer to `babashka.process/process` for the exact behavior of: :in, :out, :dir, and the exact nature of the return value.
Returns a record with (among other things, see `babashka.process/process`):
- `:out` the output stream
- `:exit` the exit code of sops
- `:err` a string containing the stderr output from sops, if any
If you `clojure.core/deref` the record, then it will block until the process has exited.
If you slurp from the out stream, it will also block until the process has exited.
Usage examples
--------------
Decrypt file in place
@(sops :decrypt ["secrets.sops.yaml"] :in-place)
Decrypt file to a string
(slurp (:out (sops :decrypt ["secrets.sops.yaml"])))
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/v0.1.x/src/ol/sops.clj#L6-L46)
---
## decrypt-file-to-str
```clojure
(decrypt-file-to-str file opts)
```
Decrypts the path `file` and returns the output as a string. Sugar over [`sops`](#sops).
Options:
- `:input-type` - Override file type detection (yaml, json, env, ini, binary)
- `:output-type` - Override output format (yaml, json, env, ini, binary)
- Other SOPS options as documented in `sops decrypt --help`
Example:
```clojure
(decrypt-file "secrets.sops.yaml" {})
(decrypt-file "secrets.enc" {:input-type "yaml" :output-type "json"})
```
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/v0.1.x/src/ol/sops.clj#L48-L62)
---
## encrypt-to-file
```clojure
(encrypt-to-file file plaintext opts)
```
Encrypts `plaintext` string and writes encrypted content to `file`. Sugar over [`sops`](#sops).
The `file` path is used to match creation rules in `.sops.yaml` and determine the file format.
Options:
- `:age` - Age recipient(s) to encrypt for (can be a single string or vector of strings)
- `:pgp` - PGP fingerprint(s) to encrypt for (can be a single string or vector of strings)
- `:gcp-kms` - GCP KMS resource ID(s) to encrypt with (can be a single string or vector of strings)
- `:azure-kv` - Azure Key Vault URL(s) to encrypt with (can be a single string or vector of strings)
- `:kms` - AWS KMS ARN(s) to encrypt with (can be a single string or vector of strings)
- `:aws-profile` - The AWS profile to use for requests to AWS
- Other SOPS options as documented in `sops encrypt --help`
Example:
```clojure
(encrypt-to-file "secrets.sops.yaml" "foo: bar" {:age "age1..."})
```
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/v0.1.x/src/ol/sops.clj#L64-L83)
---
## with-process-opts
_macro_
```clojure
(with-process-opts env-map & body)
```
An escape hatch that executes body with additional babashka/process options merged into the process call.
Example:
```clojure
;; Override the environemnt
(with-process-opts {:extra-env {:SOPS_AGE_KEY_FILE "/some/path/to/keys.txt"}}
(sops :decrypt ["secrets.yaml"]))
```
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/v0.1.x/src/ol/sops.clj#L85-L96)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## ++[++UNRELEASED++]++
## `v0.1.0` (2025-10-03)
This is the first public release of this codebase under the `ol.sops`
name.
Please report any problems and let me know if anything is unclear,
inconvenient, etc. Thank you! 🙏
##
ol.sops
# `ol.sops`
> An extremely tiny and simple wrapper around the awesome sops (previously
> known as mozilla/sops)



https://clojars.org/com.outskirtslabs/sops[image:https://img.shields.io/clojars/v/com.outskirtslabs/sops.svg[Clojars
Project]]
This is intended for [babashka](https://babashka.org/) and JVM clojure and
provides an idiomatic and data driven wrapper around the CLI tool.
Project status: **[Maturing](https://docs.outskirtslabs.com/open-source-vital-signs#maturing)**.
## Installation
```clojure
{:deps {com.outskirtslabs/sops {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/sops "0.1.0"]
```
## Quick Start
```clojure
(ns myapp.core
(:require [ol.sops :as sops]))
;; decrypt a sops file to string
(sops/decrypt-file-to-str "dev/test.sops.yml"
{:env {"SOPS_AGE_KEY_FILE" "dev/keys.txt"}})
;; => "hello: world\n"
;; encrypt a plaintext to a sfile
@(sops/encrypt-to-file "output.sops.json"
(edn->json {:foo "bar"})
{:age "age15905pjs5av9nyh8rdt4zrzn7x0mdud20eyf7tsvz63mygvsfhd9sclsh94"})
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.sops/0.1/)
* [API Reference](https://docs.outskirtslabs.com/ol.sops/0.1/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/sops/issues)
## Security
See [here](https://github.com/outskirtslabs/sops/security) for security
advisories or to report a security vulnerability.
## License
Copyright © 2025 Casey Link casey@outskirtslabs.com
Distributed under the [MIT License](./LICENSE)
## Security policy
# Security policy
## Advisories
All security advisories will be posted
https://github.com/outskirtslabs/sops/security/advisories[on
GitHub].
## Reporting a vulnerability
Please report possible security vulnerabilities
https://github.com/outskirtslabs/sops/security/advisories[via
GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt
emails with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see
[Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
— [Casey Link](https://casey.link)
## ol.sops
# ol.sops
## sops
```clojure
(sops cmd)
(sops cmd args-or-opts)
(sops cmd args & opts)
```
A simple wrapper around babashka.process for calling sops.
You can call `sops` like this:
(sops :command ["args vector"] :opt "value" :opt2 "value2")
There are several special opts, that are handled by ol.sops:
:in - An input stream than can be passed to sops
:out - :string if you want to get a string back, nil (the default) will return an output stream you can slurp
:dir - The working directory. Defaults to the current working directory
:sops-path - Path to the sops binary. Defaults to `sops`
:env - a map of environment variables that are passed to the process
All other opts are passed to the `sops` CLI. Consult `sops CMD --help` to see available options.
To pass flags like `--verbose` use `:verbose true`. Always use full names of the options, not the short versions like -v.
Refer to `babashka.process/process` for the exact behavior of: :in, :out, :dir, and the exact nature of the return value.
Returns a record with (among other things, see `babashka.process/process`):
- `:out` the output stream
- `:exit` the exit code of sops
- `:err` a string containing the stderr output from sops, if any
If you `clojure.core/deref` the record, then it will block until the process has exited.
If you slurp from the out stream, it will also block until the process has exited.
Usage examples
--------------
Decrypt file in place
@(sops :decrypt ["secrets.sops.yaml"] :in-place)
Decrypt file to a string
(slurp (:out (sops :decrypt ["secrets.sops.yaml"])))
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/main/src/ol/sops.clj#L6-L46)
---
## decrypt-file-to-str
```clojure
(decrypt-file-to-str file opts)
```
Decrypts the path `file` and returns the output as a string. Sugar over [`sops`](#sops).
Options:
- `:input-type` - Override file type detection (yaml, json, env, ini, binary)
- `:output-type` - Override output format (yaml, json, env, ini, binary)
- Other SOPS options as documented in `sops decrypt --help`
Example:
```clojure
(decrypt-file "secrets.sops.yaml" {})
(decrypt-file "secrets.enc" {:input-type "yaml" :output-type "json"})
```
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/main/src/ol/sops.clj#L48-L62)
---
## encrypt-to-file
```clojure
(encrypt-to-file file plaintext opts)
```
Encrypts `plaintext` string and writes encrypted content to `file`. Sugar over [`sops`](#sops).
The `file` path is used to match creation rules in `.sops.yaml` and determine the file format.
Options:
- `:age` - Age recipient(s) to encrypt for (can be a single string or vector of strings)
- `:pgp` - PGP fingerprint(s) to encrypt for (can be a single string or vector of strings)
- `:gcp-kms` - GCP KMS resource ID(s) to encrypt with (can be a single string or vector of strings)
- `:azure-kv` - Azure Key Vault URL(s) to encrypt with (can be a single string or vector of strings)
- `:kms` - AWS KMS ARN(s) to encrypt with (can be a single string or vector of strings)
- `:aws-profile` - The AWS profile to use for requests to AWS
- Other SOPS options as documented in `sops encrypt --help`
Example:
```clojure
(encrypt-to-file "secrets.sops.yaml" "foo: bar" {:age "age1..."})
```
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/main/src/ol/sops.clj#L64-L83)
---
## with-process-opts
_macro_
```clojure
(with-process-opts env-map & body)
```
An escape hatch that executes body with additional babashka/process options merged into the process call.
Example:
```clojure
;; Override the environemnt
(with-process-opts {:extra-env {:SOPS_AGE_KEY_FILE "/some/path/to/keys.txt"}}
(sops :decrypt ["secrets.yaml"]))
```
[source,window=_blank](https://github.com/outskirtslabs/sops/blob/main/src/ol/sops.clj#L85-L96)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses https://www.taoensso.com/break-versioning[*Break
Versioning*].
## ++[++UNRELEASED++]++
## `v0.1.0` (2025-10-03)
This is the first public release of this codebase under the `ol.sops`
name.
Please report any problems and let me know if anything is unclear,
inconvenient, etc. Thank you! 🙏
## ol.sops
# `ol.sops`
> An extremely tiny and simple wrapper around the awesome sops (previously
> known as mozilla/sops)



https://clojars.org/com.outskirtslabs/sops[image:https://img.shields.io/clojars/v/com.outskirtslabs/sops.svg[Clojars
Project]]
This is intended for [babashka](https://babashka.org/) and JVM clojure and
provides an idiomatic and data driven wrapper around the CLI tool.
Project status: **[Maturing](https://docs.outskirtslabs.com/open-source-vital-signs#maturing)**.
## Installation
```clojure
;; deps.edn
{:deps {com.outskirtslabs/sops {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/sops "0.1.0"]
```
## Quick Start
```clojure
(ns myapp.core
(:require [ol.sops :as sops]))
;; decrypt a sops file to string
(sops/decrypt-file-to-str "dev/test.sops.yml"
{:env {"SOPS_AGE_KEY_FILE" "dev/keys.txt"}})
;; => "hello: world\n"
;; encrypt a plaintext to a sfile
@(sops/encrypt-to-file "output.sops.json"
(edn->json {:foo "bar"})
{:age "age15905pjs5av9nyh8rdt4zrzn7x0mdud20eyf7tsvz63mygvsfhd9sclsh94"})
```
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.sops/next/)
* [API Reference](https://docs.outskirtslabs.com/ol.sops/next/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/sops/issues)
## Security
See [here](https://github.com/outskirtslabs/sops/security) for security
advisories or to report a security vulnerability.
## License
Copyright © 2025 Casey Link casey@outskirtslabs.com
Distributed under the [MIT License](./LICENSE)
## Security policy
# Security policy
## Advisories
All security advisories will be posted
https://github.com/outskirtslabs/sops/security/advisories[on
GitHub].
## Reporting a vulnerability
Please report possible security vulnerabilities
https://github.com/outskirtslabs/sops/security/advisories[via
GitHub], or by emailing me at `casey@outskirtslabs.com`. You may encrypt
emails with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see
[Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
— [Casey Link](https://casey.link)
## ol.trixnity.client
# ol.trixnity.client
Matrix client lifecycle, sync control, and client-level state flows.
## Upstream Mapping
This namespace maps to client-wide APIs on Trixnity’s
`de.connect2x.trixnity.client.MatrixClient`.
The public wrappers here cover:
* opening or resuming a client with [`open`](#open)
* sync lifecycle tasks such as [`start-sync`](#start-sync), [`await-running`](#await-running),
[`stop-sync`](#stop-sync), and [`close`](#close)
* synchronous `current-*` accessors paired with relieved Missionary flows
for profile, server data, sync state, login state, and startup state
Use [`ol.trixnity.repo`](api/ol-trixnity-repo.adoc) when you want the built-in sqlite4clj-backed
repository setup, and [`ol.trixnity.room`](api/ol-trixnity-room.adoc), [`ol.trixnity.user`](api/ol-trixnity-user.adoc),
[`ol.trixnity.notification`](api/ol-trixnity-notification.adoc), [`ol.trixnity.verification`](api/ol-trixnity-verification.adoc), and
[`ol.trixnity.key`](api/ol-trixnity-key.adoc) for service-specific APIs.
## open
```clojure
(open config)
```
Opens or resumes a `MatrixClient` using the built-in sqlite4clj-backed
repository configuration.
If `config` already contains a client under `::mx/client`, the returned task
resolves to that client immediately.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L39-L50)
---
## start-sync
```clojure
(start-sync client)
```
Starts sync for `client` and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L52-L55)
---
## await-running
```clojure
(await-running client)
(await-running client opts)
```
Waits for `client` to reach `RUNNING`.
Supported opts:
`{::mx/timeout java.time.Duration}`
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L57-L69)
---
## stop-sync
```clojure
(stop-sync client)
```
Stops sync for `client` and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L71-L74)
---
## close
```clojure
(close client)
```
Closes `client` and tears down its bridge-owned coroutine scope.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L76-L79)
---
## current-user-id
```clojure
(current-user-id client)
```
Returns the Matrix user id of `client` as a string.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L81-L84)
---
## current-sync-state
```clojure
(current-sync-state client)
```
Returns the current sync state as a lower-case keyword.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L86-L89)
---
## current-profile
```clojure
(current-profile client)
```
Returns the current normalized client profile.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L91-L94)
---
## profile
```clojure
(profile client)
```
Returns a Missionary flow of the current normalized client profile.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L96-L100)
---
## current-server-data
```clojure
(current-server-data client)
```
Returns the current normalized server data snapshot.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L102-L105)
---
## server-data
```clojure
(server-data client)
```
Returns a Missionary flow of normalized server data snapshots.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L107-L111)
---
## sync-state
```clojure
(sync-state client)
```
Returns a Missionary flow of the current sync state.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L113-L118)
---
## current-initial-sync-done
```clojure
(current-initial-sync-done client)
```
Returns whether initial sync has completed.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L120-L123)
---
## initial-sync-done
```clojure
(initial-sync-done client)
```
Returns a Missionary flow of initial-sync completion state.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L125-L129)
---
## current-login-state
```clojure
(current-login-state client)
```
Returns the current login state as a lower-case keyword or nil.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L131-L134)
---
## login-state
```clojure
(login-state client)
```
Returns a Missionary flow of lower-case login-state keywords or nil.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L136-L141)
---
## current-started
```clojure
(current-started client)
```
Returns whether sync has been started for `client`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L143-L146)
---
## started
```clojure
(started client)
```
Returns a Missionary flow of `client` started state.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj#L148-L152)
## ol.trixnity.event
# ol.trixnity.event
Accessors and predicates for normalized event maps.
This namespace provides convenience accessors over
the normalized event shapes defined in [`ol.trixnity.schemas`](api/ol-trixnity-schemas.adoc) without
exposing callers to raw bridge internals.
Use these helpers when consuming timeline events, notifications, or reply
metadata from [`ol.trixnity.room`](api/ol-trixnity-room.adoc), [`ol.trixnity.notification`](api/ol-trixnity-notification.adoc), and
[`ol.trixnity.room.message`](api/ol-trixnity-room-message.adoc).
## type
```clojure
(type event)
```
Returns the Matrix event type string from normalized `event`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L17-L20)
---
## room-id
```clojure
(room-id event)
```
Returns the Matrix room id string from normalized `event`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L22-L25)
---
## event-id
```clojure
(event-id event)
```
Returns the Matrix event id string from normalized `event`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L27-L30)
---
## sender
```clojure
(sender event)
```
Returns the sender user id string from normalized `event`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L32-L35)
---
## sender-display-name
```clojure
(sender-display-name event)
```
Returns the sender display name from normalized `event`, if present.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L37-L40)
---
## body
```clojure
(body event)
```
Returns the normalized message body from `event`, if present.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L42-L45)
---
## key
```clojure
(key event)
```
Returns the normalized reaction key or relation key from `event`, if present.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L47-L50)
---
## relates-to
```clojure
(relates-to event)
```
Returns the normalized `::mx/relates-to` map from `event`, if present.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L52-L55)
---
## relation-event-id
```clojure
(relation-event-id event)
```
Returns the related event id from `event` reply or relation metadata, if present.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L57-L60)
---
## raw
```clojure
(raw event)
```
Returns the upstream raw event object carried by normalized `event`, if present.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L62-L65)
---
## text?
```clojure
(text? event)
```
Returns true when `event` is a Matrix room-message event.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L67-L70)
---
## reaction?
```clojure
(reaction? event)
```
Returns true when `event` is a Matrix reaction event.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/event.clj#L72-L75)
## ol.trixnity.internal.bridge
# ol.trixnity.internal.bridge
## open-client
```clojure
(open-client request on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L17-L18)
---
## start-sync
```clojure
(start-sync client on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L20-L21)
---
## await-running
```clojure
(await-running client timeout on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L23-L24)
---
## stop-sync
```clojure
(stop-sync client on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L26-L27)
---
## close-client
```clojure
(close-client client on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L29-L30)
---
## current-user-id
```clojure
(current-user-id client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L32-L33)
---
## current-sync-state
```clojure
(current-sync-state client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L35-L36)
---
## current-profile
```clojure
(current-profile client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L38-L39)
---
## profile-flow
```clojure
(profile-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L41-L42)
---
## current-server-data
```clojure
(current-server-data client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L44-L45)
---
## server-data-flow
```clojure
(server-data-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L47-L48)
---
## sync-state-flow
```clojure
(sync-state-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L50-L51)
---
## current-initial-sync-done
```clojure
(current-initial-sync-done client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L53-L54)
---
## initial-sync-done-flow
```clojure
(initial-sync-done-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L56-L57)
---
## current-login-state
```clojure
(current-login-state client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L59-L60)
---
## login-state-flow
```clojure
(login-state-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L62-L63)
---
## current-started
```clojure
(current-started client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L65-L66)
---
## started-flow
```clojure
(started-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L68-L69)
---
## current-users-typing
```clojure
(current-users-typing client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L71-L72)
---
## users-typing-flow
```clojure
(users-typing-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L74-L75)
---
## create-room
```clojure
(create-room client room-name on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L77-L78)
---
## invite-user
```clojure
(invite-user client room-id user-id timeout on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L80-L81)
---
## join-room
```clojure
(join-room client room-id timeout on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L83-L84)
---
## forget-room
```clojure
(forget-room client room-id force on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L86-L87)
---
## send-message
```clojure
(send-message client room-id message timeout on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L89-L90)
---
## send-reaction
```clojure
(send-reaction client room-id event-id key timeout on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L92-L93)
---
## cancel-send-message
```clojure
(cancel-send-message client room-id transaction-id on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L95-L96)
---
## retry-send-message
```clojure
(retry-send-message client room-id transaction-id on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L98-L99)
---
## room-by-id
```clojure
(room-by-id client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L101-L102)
---
## rooms
```clojure
(rooms client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L104-L105)
---
## rooms-flat
```clojure
(rooms-flat client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L107-L108)
---
## account-data
```clojure
(account-data client room-id event-content-class key)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L110-L111)
---
## state
```clojure
(state client room-id event-content-class state-key)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L113-L114)
---
## all-state
```clojure
(all-state client room-id event-content-class)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L116-L117)
---
## outbox
```clojure
(outbox client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L119-L120)
---
## outbox-flat
```clojure
(outbox-flat client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L122-L123)
---
## outbox-by-room
```clojure
(outbox-by-room client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L125-L126)
---
## outbox-by-room-flat
```clojure
(outbox-by-room-flat client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L128-L129)
---
## outbox-message
```clojure
(outbox-message client room-id transaction-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L131-L132)
---
## fill-timeline-gaps
```clojure
(fill-timeline-gaps client room-id event-id limit on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L134-L135)
---
## timeline-events-from-now-on
```clojure
(timeline-events-from-now-on client decryption-timeout-ms sync-response-buffer-size)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L137-L142)
---
## response-timeline-events
```clojure
(response-timeline-events client response decryption-timeout-ms)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L144-L145)
---
## timeline-event
```clojure
(timeline-event client room-id event-id decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L147-L156)
---
## previous-timeline-event
```clojure
(previous-timeline-event client timeline-event decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L158-L166)
---
## next-timeline-event
```clojure
(next-timeline-event client timeline-event decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L168-L176)
---
## last-timeline-event
```clojure
(last-timeline-event client room-id decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L178-L186)
---
## timeline-event-chain
```clojure
(timeline-event-chain client room-id start-from direction decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content min-size max-size)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L188-L200)
---
## last-timeline-events
```clojure
(last-timeline-events client room-id decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content min-size max-size)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L202-L212)
---
## timeline-events-list
```clojure
(timeline-events-list client room-id start-from direction max-size min-size decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L214-L226)
---
## last-timeline-events-list
```clojure
(last-timeline-events-list client room-id max-size min-size decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L228-L238)
---
## timeline-events-around
```clojure
(timeline-events-around client room-id start-from max-size-before max-size-after decryption-timeout-ms fetch-timeout-ms fetch-size allow-replace-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L240-L251)
---
## timeline-event-relations
```clojure
(timeline-event-relations client room-id event-id relation-type)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L253-L254)
---
## user-all
```clojure
(user-all client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L256-L257)
---
## load-members
```clojure
(load-members client room-id wait on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L259-L260)
---
## user-by-id
```clojure
(user-by-id client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L262-L263)
---
## user-all-receipts
```clojure
(user-all-receipts client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L265-L266)
---
## user-receipts-by-id
```clojure
(user-receipts-by-id client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L268-L269)
---
## user-power-level
```clojure
(user-power-level client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L271-L272)
---
## can-kick-user
```clojure
(can-kick-user client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L274-L275)
---
## can-ban-user
```clojure
(can-ban-user client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L277-L278)
---
## can-unban-user
```clojure
(can-unban-user client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L280-L281)
---
## can-invite-user
```clojure
(can-invite-user client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L283-L284)
---
## can-invite
```clojure
(can-invite client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L286-L287)
---
## can-redact-event
```clojure
(can-redact-event client room-id event-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L289-L290)
---
## can-set-power-level-to-max
```clojure
(can-set-power-level-to-max client room-id user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L292-L293)
---
## can-send-event-by-class
```clojure
(can-send-event-by-class client room-id event-content-class)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L295-L296)
---
## can-send-event-by-content
```clojure
(can-send-event-by-content client room-id event-content)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L298-L299)
---
## user-presence
```clojure
(user-presence client user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L301-L302)
---
## user-account-data
```clojure
(user-account-data client event-content-class key)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L304-L305)
---
## notifications
```clojure
(notifications client decryption-timeout-ms sync-response-buffer-size)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L307-L308)
---
## notifications-from-response
```clojure
(notifications-from-response client response decryption-timeout-ms)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L310-L311)
---
## notification-all
```clojure
(notification-all client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L313-L314)
---
## notification-all-flat
```clojure
(notification-all-flat client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L316-L317)
---
## notification-by-id
```clojure
(notification-by-id client id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L319-L320)
---
## notification-count
```clojure
(notification-count client)
(notification-count client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L322-L326)
---
## notification-unread
```clojure
(notification-unread client room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L328-L329)
---
## notification-all-updates
```clojure
(notification-all-updates client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L331-L332)
---
## notification-dismiss
```clojure
(notification-dismiss client id on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L334-L335)
---
## notification-dismiss-all
```clojure
(notification-dismiss-all client on-success on-failure)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L337-L338)
---
## current-active-device-verification
```clojure
(current-active-device-verification client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L340-L341)
---
## active-device-verification-flow
```clojure
(active-device-verification-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L343-L344)
---
## current-active-user-verifications
```clojure
(current-active-user-verifications client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L346-L347)
---
## active-user-verifications-flow
```clojure
(active-user-verifications-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L349-L350)
---
## self-verification-methods
```clojure
(self-verification-methods client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L352-L353)
---
## current-bootstrap-running
```clojure
(current-bootstrap-running client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L355-L356)
---
## bootstrap-running-flow
```clojure
(bootstrap-running-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L358-L359)
---
## current-backup-version
```clojure
(current-backup-version client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L361-L362)
---
## backup-version-flow
```clojure
(backup-version-flow client)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L364-L365)
---
## device-trust-level
```clojure
(device-trust-level client user-id device-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L367-L368)
---
## timeline-trust-level
```clojure
(timeline-trust-level client room-id event-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L370-L371)
---
## user-trust-level
```clojure
(user-trust-level client user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L373-L374)
---
## device-keys-flow
```clojure
(device-keys-flow client user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L376-L377)
---
## cross-signing-keys-flow
```clojure
(cross-signing-keys-flow client user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal/bridge.clj#L379-L380)
## ol.trixnity.internal
# ol.trixnity.internal
## observe-flow
```clojure
(observe-flow client kotlin-flow)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal.clj#L17-L21)
---
## observe-keyed-flow-map
```clojure
(observe-keyed-flow-map client kotlin-outer-flow)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal.clj#L23-L40)
---
## observe-flow-list
```clojure
(observe-flow-list client kotlin-outer-flow)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal.clj#L42-L47)
---
## suspend-task
```clojure
(suspend-task bridge-fn & args)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal.clj#L49-L52)
---
## duration->millis
```clojure
(duration->millis duration)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/internal.clj#L54-L55)
## ol.trixnity.key
# ol.trixnity.key
Encryption-key state, backup state, and trust-level queries.
## Upstream Mapping
This namespace maps to Trixnity’s `KeyService` and `KeyBackupService`.
The public wrappers here cover:
* current and flow-based backup bootstrap state
* current and flow-based backup-version observation
* trust-level queries for users, devices, and timeline events
* device-key and cross-signing-key lookup
Use [`ol.trixnity.verification`](api/ol-trixnity-verification.adoc) for active verification workflows and
[`ol.trixnity.room`](api/ol-trixnity-room.adoc) when you need room timeline events to pair with
trust-level checks.
## current-bootstrap-running
```clojure
(current-bootstrap-running client)
```
Returns whether cross-signing bootstrap is currently running.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L27-L30)
---
## bootstrap-running
```clojure
(bootstrap-running client)
```
Returns a Missionary flow of cross-signing bootstrap state.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L32-L36)
---
## current-backup-version
```clojure
(current-backup-version client)
```
Returns the current active room-key-backup version, or nil when unavailable.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L38-L41)
---
## backup-version
```clojure
(backup-version client)
```
Returns a Missionary flow of the active room-key-backup version.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L43-L47)
---
## get-trust-level
```clojure
(get-trust-level client user-id)
(get-trust-level client user-id-or-room-id device-id-or-event-id)
```
Returns a Missionary flow of trust information.
With two arguments, `user-id` resolves to the trust level of that user.
With three arguments, the meaning depends on the first id:
* a `@user:server` value resolves device trust for `device-id`
* a `!room:server` value resolves the trust level for the device that sent
the timeline `event-id`
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L49-L71)
---
## get-device-keys
```clojure
(get-device-keys client user-id)
```
Returns a Missionary flow of device keys for `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L73-L77)
---
## get-cross-signing-keys
```clojure
(get-cross-signing-keys client user-id)
```
Returns a Missionary flow of cross-signing keys for `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/key.clj#L79-L83)
## ol.trixnity.notification
# ol.trixnity.notification
Notification snapshots and notification update flows.
## Upstream Mapping
This namespace maps to Trixnity’s `NotificationService`.
The public wrappers here cover:
* observing the full notification set in keyed or flat shapes
* looking up individual notifications and unread counts
* consuming live notification updates
* a deprecated bridge for notification extraction from sync responses
Use [`ol.trixnity.room`](api/ol-trixnity-room.adoc) for room timeline access and
[`ol.trixnity.client`](api/ol-trixnity-client.adoc) for client lifecycle.
## get-notifications
```clojure
(get-notifications client)
(get-notifications client response-or-opts)
(get-notifications client response opts)
```
Returns deprecated notification extraction flows.
Prefer [`get-all`](#get-all), [`get-by-id`](#get-by-id), and [`get-all-updates`](#get-all-updates) for the current
notification model.
Supported opts:
| key | description |
|----------------------------------|------------------------------------------------------------------|
| `::mx/decryption-timeout` | Decryption timeout for derived timeline events |
| `::mx/sync-response-buffer-size` | Number of sync responses buffered while extracting notifications |
_deprecated_
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L26-L58)
---
## get-all
```clojure
(get-all client)
```
Returns a Missionary flow of the current notifications as a list of inner flows.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L60-L63)
---
## get-all-flat
```clojure
(get-all-flat client)
```
Returns a Missionary flow of flattened notification snapshots.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L65-L68)
---
## get-by-id
```clojure
(get-by-id client id)
```
Returns a Missionary flow of the notification with `id`, or nil when unavailable.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L70-L74)
---
## get-count
```clojure
(get-count client)
(get-count client room-id)
```
Returns a Missionary flow of notification counts.
With one argument, returns the total count across all rooms.
With `room-id`, returns the count for that room.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L76-L85)
---
## is-unread
```clojure
(is-unread client room-id)
```
Returns a Missionary flow that is true when `room-id` is considered unread.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L87-L91)
---
## dismiss
```clojure
(dismiss client id)
```
Marks the notification with `id` as dismissed and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L93-L97)
---
## dismiss-all
```clojure
(dismiss-all client)
```
Dismisses all notifications and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L99-L102)
---
## get-all-updates
```clojure
(get-all-updates client)
```
Returns a Missionary flow of notification updates.
Upstream notes that this stream should not be buffered because consumed
updates are removed from the backing store as new values are requested.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/notification.clj#L104-L110)
## ol.trixnity.repo.common
# ol.trixnity.repo.common
## db
```clojure
(db handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L48-L50)
---
## json
```clojure
(json handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L52-L54)
---
## writer-pool
```clojure
(writer-pool handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L56-L58)
---
## reader-pool
```clojure
(reader-pool handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L60-L62)
---
## borrow-reader-conn!
```clojure
(borrow-reader-conn! handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L64-L66)
---
## borrow-writer-conn!
```clojure
(borrow-writer-conn! handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L68-L70)
---
## release-reader-conn!
```clojure
(release-reader-conn! handle conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L72-L75)
---
## release-writer-conn!
```clojure
(release-writer-conn! handle conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L77-L80)
---
## begin-deferred!
```clojure
(begin-deferred! conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L82-L85)
---
## begin-immediate!
```clojure
(begin-immediate! conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L87-L90)
---
## commit!
```clojure
(commit! conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L92-L95)
---
## rollback!
```clojure
(rollback! conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L97-L100)
---
## current-read-conn
```clojure
(current-read-conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L102-L104)
---
## current-write-conn
```clojure
(current-write-conn)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L106-L108)
---
## q-read
```clojure
(q-read handle query)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L110-L115)
---
## q-write
```clojure
(q-write handle query)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L117-L121)
---
## open!
```clojure
(open! path json)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L123-L125)
---
## ensure-schema!
```clojure
(ensure-schema! handle)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L127-L131)
---
## encode-json
```clojure
(encode-json json serializer value)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L133-L135)
---
## decode-json
```clojure
(decode-json json serializer value)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L137-L139)
---
## first-row
```clojure
(first-row rows)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L141-L144)
---
## maybe-rows
```clojure
(maybe-rows rows)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L146-L148)
---
## in-placeholders
```clojure
(in-placeholders n)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L150-L152)
---
## minimal-get
```clojure
(minimal-get handle table repo-key serializer)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L154-L160)
---
## minimal-save!
```clojure
(minimal-save! handle table repo-key payload)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L162-L169)
---
## minimal-delete!
```clojure
(minimal-delete! handle table repo-key)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L171-L176)
---
## delete-all!
```clojure
(delete-all! handle table)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L178-L182)
---
## full-get-all
```clojure
(full-get-all handle table serializer)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L184-L189)
---
## room-id-str
```clojure
(room-id-str room-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L191-L193)
---
## user-id-str
```clojure
(user-id-str user-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L195-L197)
---
## event-id-str
```clojure
(event-id-str event-id)
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo/common.clj#L199-L201)
## ol.trixnity.repo
# ol.trixnity.repo
Built-in repository helpers for the sqlite4clj-backed repository setup.
Most callers do not need to interact with the raw repository handle or the
Kotlin bridge types directly. Use [`sqlite4clj-config`](#sqlite4clj-config) with
[`ol.trixnity.client/open`](api/ol-trixnity-client.adoc#open).
Advanced callers can still build a `MatrixClient` with some other repository
implementation and pass that client to [`ol.trixnity.client/open`](api/ol-trixnity-client.adoc#open) via
`::mx/client`.
## sqlite4clj-config
```clojure
(sqlite4clj-config options)
```
Returns config entries for the built-in sqlite4clj repository and okio media store.
Prefer the namespaced keys from [`ol.trixnity.schemas`](api/ol-trixnity-schemas.adoc). Plain keywords are
still accepted here as a convenience when normalizing app config.
Options:
| key | description
|-----|-------------
| `::mx/database-path` | SQLite file path used by `ol.trixnity.repo`
| `::mx/media-path` | Directory used by the okio-backed media store
Example:
```clojure
(m/? (client/open
(merge
{::mx/homeserver-url "https://matrix.example.org"
::mx/username "bot"
::mx/password "secret"}
(repo/sqlite4clj-config
{::mx/database-path "./var/trixnity.sqlite"
::mx/media-path "./var/media"}))))
```
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo.clj#L47-L74)
---
## open-handle!
```clojure
(open-handle! path json)
```
Opens a low-level sqlite4clj repository handle for `path`.
Most callers should prefer [`sqlite4clj-config`](#sqlite4clj-config) with
[`ol.trixnity.client/open`](api/ol-trixnity-client.adoc#open). This is the lower-level escape hatch used by the
built-in repository assembly.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo.clj#L76-L83)
---
## create-repositories
```clojure
(create-repositories handle mappings)
```
Builds the repository map expected by the Kotlin bridge from `handle`.
This is primarily a low-level integration seam for custom client assembly.
Most callers should use [`sqlite4clj-config`](#sqlite4clj-config) instead of constructing this
map directly.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo.clj#L789-L827)
## ol.trixnity.room.message
# ol.trixnity.room.message
Helpers for constructing normalized room message payloads.
This namespace provides small builders for the message-spec maps accepted by
[`ol.trixnity.room/send-message`](api/ol-trixnity-room.adoc#send-message).
The helpers here focus on common text-message cases:
* [`text`](#text) builds a normalized text message payload
* [`reply-to`](#reply-to) attaches reply metadata from a normalized event map
Use [`ol.trixnity.event`](api/ol-trixnity-event.adoc) to inspect the events you are replying to.
## text
```clojure
(text body)
(text body opts)
```
Builds a text message-spec map understood by [`ol.trixnity.room/send-message`](api/ol-trixnity-room.adoc#send-message).
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room/message.clj#L19-L30)
---
## reply-to
```clojure
(reply-to message ev)
```
Associates reply metadata from normalized event `ev` onto `message`.
When `ev` already carries relation metadata, that relation is copied into the
reply target so upstream threading semantics are preserved.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room/message.clj#L32-L45)
## ol.trixnity.room
# ol.trixnity.room
Room operations, state queries, and timeline traversal.
## Upstream Mapping
This namespace maps primarily to Trixnity’s `RoomService` on
`de.connect2x.trixnity.client.MatrixClient`.
The public wrappers here cover three upstream groupings:
* room mutations such as room creation, invites, messages, and reactions
* room observation and state operations such as `getById`, `getAll`,
`getAccountData`, `getState`, and `getOutbox`
* room-scoped timeline lookup and traversal helpers exposed through the
room service and its timeline helpers
Use [`ol.trixnity.user`](api/ol-trixnity-user.adoc) for `UserService` APIs and
[`ol.trixnity.notification`](api/ol-trixnity-notification.adoc), [`ol.trixnity.verification`](api/ol-trixnity-verification.adoc), and
[`ol.trixnity.key`](api/ol-trixnity-key.adoc) for the other non-room service mappings.
## create-room
```clojure
(create-room client opts)
```
Creates a room and returns a Missionary task that resolves to the new room id.
Supported opts:
| key | description |
|------------------|-----------------------------------------|
| `::mx/room-name` | Explicit room name used during creation |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L61-L73)
---
## invite-user
```clojure
(invite-user client room-id user-id)
(invite-user client room-id user-id opts)
```
Invites `user-id` to `room-id` and returns a Missionary task.
Supported opts:
| key | description
|-----|-------------
| `::mx/timeout` | Maximum time to wait for the invite request |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L75-L93)
---
## join-room
```clojure
(join-room client room-id)
(join-room client room-id opts)
```
Joins `room-id` and returns a Missionary task.
Supported opts:
| key | description
|-----|-------------
| `::mx/timeout` | Maximum time to wait for the join request |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L95-L111)
---
## forget-room
```clojure
(forget-room client room-id)
(forget-room client room-id opts)
```
Forgets `room-id` locally and returns a Missionary task.
Upstream notes that this is intended for rooms in `LEAVE` membership.
Supported opts:
| key | description
|-----|-------------
| `::mx/force` | Force forgetting even when the usual upstream preconditions are not met |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L113-L131)
---
## send-message
```clojure
(send-message client room-id message)
(send-message client room-id message opts)
```
Queues `message` for `room-id` and returns a Missionary task of the transaction id.
Supported opts:
| key | description
|-----|-------------
| `::mx/timeout` | Maximum time to wait for the send operation |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L133-L151)
---
## send-reaction
```clojure
(send-reaction client room-id ev key)
(send-reaction client room-id ev key opts)
```
Sends a reaction to event `ev` in `room-id` and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L153-L167)
---
## cancel-send-message
```clojure
(cancel-send-message client room-id transaction-id)
```
Cancels an outbox message identified by `transaction-id` and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L169-L177)
---
## retry-send-message
```clojure
(retry-send-message client room-id transaction-id)
```
Retries an outbox message identified by `transaction-id` and returns a Missionary task.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L179-L187)
---
## get-by-id
```clojure
(get-by-id client room-id)
```
Returns a Missionary flow of the room for `room-id`, or nil when unavailable.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L189-L193)
---
## get-all
```clojure
(get-all client)
```
Returns a Missionary flow of room flows keyed by room id.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L195-L198)
---
## get-all-flat
```clojure
(get-all-flat client)
```
Returns a Missionary flow of flattened room snapshots.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L200-L203)
---
## current-users-typing
```clojure
(current-users-typing client)
```
Returns the current typing-state snapshot keyed by room id.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L205-L208)
---
## users-typing
```clojure
(users-typing client)
```
Returns a relieved Missionary flow of typing-state snapshots keyed by room id.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L210-L214)
---
## get-account-data
```clojure
(get-account-data client room-id event-content-class)
(get-account-data client room-id event-content-class key)
```
Returns a Missionary flow of room account-data content.
When `key` is omitted, the empty-string key is used.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L216-L230)
---
## get-state
```clojure
(get-state client room-id event-content-class)
(get-state client room-id event-content-class state-key)
```
Returns a Missionary flow of room state for `event-content-class`.
When `state-key` is omitted, the empty-string state key is used.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L232-L246)
---
## get-all-state
```clojure
(get-all-state client room-id event-content-class)
```
Returns a Missionary flow of state-event flows keyed by state key.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L248-L257)
---
## get-outbox
```clojure
(get-outbox client)
(get-outbox client room-id)
(get-outbox client room-id transaction-id)
```
Returns Missionary flows over room outbox state.
Arities:
* `(get-outbox client)` returns all outbox entries as a list of inner flows
* `(get-outbox client room-id)` scopes that list to one room
* `(get-outbox client room-id transaction-id)` returns the single outbox
entry flow for that transaction id
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L259-L276)
---
## get-outbox-flat
```clojure
(get-outbox-flat client)
(get-outbox-flat client room-id)
```
Returns flattened Missionary flows over room outbox state.
With `room-id`, scopes the flattened outbox view to a single room.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L278-L286)
---
## fill-timeline-gaps
```clojure
(fill-timeline-gaps client room-id event-id)
(fill-timeline-gaps client room-id event-id opts)
```
Fills timeline gaps around `event-id` in `room-id` and returns a Missionary task.
Supported opts:
| key | description
|-----|-------------
| `::mx/limit` | Maximum number of events to request while filling gaps (default `20`) |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L288-L306)
---
## get-timeline-event
```clojure
(get-timeline-event client room-id event-id)
(get-timeline-event client room-id event-id opts)
```
Returns a Missionary flow of the timeline event for `event-id`.
Upstream notes that this lookup may traverse locally stored events and fill
remote gaps when the event is not available locally.
Supported opts:
| key | description
|-----|-------------
| `::mx/decryption-timeout` | Timeout used while decrypting timeline events
| `::mx/fetch-timeout` | Timeout for remote fetches when the event is missing locally
| `::mx/fetch-size` | Maximum number of events fetched from the server at once
| `::mx/allow-replace-content` | Replace event content when an `m.replace` relation is present |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L308-L338)
---
## get-previous-timeline-event
```clojure
(get-previous-timeline-event client timeline-event)
(get-previous-timeline-event client timeline-event opts)
```
Returns a Missionary flow of the previous timeline event relative to `timeline-event`.
Returns nil when upstream cannot traverse backward from the supplied event.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L340-L356)
---
## get-next-timeline-event
```clojure
(get-next-timeline-event client timeline-event)
(get-next-timeline-event client timeline-event opts)
```
Returns a Missionary flow of the next timeline event relative to `timeline-event`.
Returns nil when upstream cannot traverse forward from the supplied event.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L358-L374)
---
## get-last-timeline-event
```clojure
(get-last-timeline-event client room-id)
(get-last-timeline-event client room-id opts)
```
Returns a Missionary outer flow whose values are flows of the latest timeline event.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L376-L392)
---
## get-timeline-events
```clojure
(get-timeline-events client response)
(get-timeline-events client response opts)
(get-timeline-events client room-id start-from direction)
(get-timeline-events client room-id start-from direction opts)
```
Returns Missionary flows over timeline events.
Arities:
* `(get-timeline-events client response opts)` extracts timeline events from
a sync `response`
* `(get-timeline-events client room-id start-from direction opts)` traverses
a room timeline from `start-from` in `:backwards` or `:forwards`
The room traversal arity follows upstream behavior: it emits flows of events,
may fetch missing events from the server, and can be bounded with
`::mx/min-size` and `::mx/max-size`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L394-L437)
---
## get-last-timeline-events
```clojure
(get-last-timeline-events client room-id)
(get-last-timeline-events client room-id opts)
```
Returns a Missionary flow whose values are flows of flows for the latest timeline events.
This mirrors upstream’s nested-flow shape for continuously updated room-end
traversal.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L439-L460)
---
## get-timeline-events-list
```clojure
(get-timeline-events-list client room-id start-from direction max-size min-size)
(get-timeline-events-list client room-id start-from direction max-size min-size opts)
```
Returns a Missionary flow of timeline-event lists starting from `start-from`.
`max-size` and `min-size` bound the list-shaped traversal directly.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L462-L488)
---
## get-last-timeline-events-list
```clojure
(get-last-timeline-events-list client room-id max-size min-size)
(get-last-timeline-events-list client room-id max-size min-size opts)
```
Returns a Missionary flow of the latest timeline events as lists.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L490-L510)
---
## get-timeline-events-around
```clojure
(get-timeline-events-around client room-id start-from max-size-before max-size-after)
(get-timeline-events-around client room-id start-from max-size-before max-size-after opts)
```
Returns a Missionary flow of timeline-event lists centered around `start-from`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L512-L534)
---
## get-timeline-events-from-now-on
```clojure
(get-timeline-events-from-now-on client)
(get-timeline-events-from-now-on client opts)
```
Returns a Missionary flow of timeline events received after subscription starts.
Upstream notes that timeline gaps are not filled automatically for this live
stream.
Supported opts:
| key | description
|-----|-------------
| `::mx/decryption-timeout` | Timeout used while decrypting live events
| `::mx/sync-response-buffer-size` | Number of sync responses buffered while events are consumed |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L536-L557)
---
## get-timeline-event-relations
```clojure
(get-timeline-event-relations client room-id event-id relation-type)
```
Returns a Missionary flow of related timeline-event flows keyed by related event id.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/room.clj#L559-L567)
## ol.trixnity.schemas
# ol.trixnity.schemas
Malli schema registry for ol.trixnity
Public namespaces validate arguments and normalize bridge-shaped data through
the schemas defined here before crossing into
[`ol.trixnity.internal.bridge`](api/ol-trixnity-internal-bridge.adoc).
The registry includes:
* public request and options maps for client, room, notification, and key
operations
* normalized event, profile, room, notification, verification, and trust
data shapes
* helper functions for building a registry and enforcing schema validation
Most callers use these namespaced keys indirectly through the higher-level
public APIs. Reach for this namespace directly when constructing config or
payload maps by hand.
## schemas
```clojure
(schemas _)
```
Returns the project Malli schema map for `opts`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/schemas.clj#L45-L400)
---
## registry
```clojure
(registry opts)
```
Builds a Malli registry containing the project schemas and default schemas.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/schemas.clj#L402-L407)
---
## schema-registry
Default Malli registry used by public API validation.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/schemas.clj#L409-L411)
---
## validate!
```clojure
(validate! schema-id data)
(validate! registry schema-id data)
```
Validates `data` against `schema-id` and returns `data` on success.
Throws `ExceptionInfo` with humanized Malli errors on validation failure.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/schemas.clj#L416-L430)
## ol.trixnity.user
# ol.trixnity.user
Room user state, receipts, presence, and permission checks.
## Upstream Mapping
This namespace wraps the user-oriented room APIs exposed by Trixnity’s
`RoomService`.
The public wrappers here cover:
* room member lookup by id or as keyed flow maps
* per-user receipt and power-level observation
* permission checks for invites, bans, kicks, redactions, power changes, and
sending events
* user presence and global account-data queries
Use [`ol.trixnity.room`](api/ol-trixnity-room.adoc) for room lifecycle and timeline access, and
[`ol.trixnity.client`](api/ol-trixnity-client.adoc) for client-wide state.
## load-members
```clojure
(load-members client room-id)
(load-members client room-id opts)
```
Loads room members for `room-id` and returns a Missionary task.
Supported opts:
| key | description
|-----|-------------
| `::mx/wait` | When true, wait for the member load to finish before resolving the task (default `true`) |
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L26-L42)
---
## get-all
```clojure
(get-all client room-id)
```
Returns a Missionary flow of room members keyed by user id.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L44-L48)
---
## get-by-id
```clojure
(get-by-id client room-id user-id)
```
Returns a Missionary flow of the room member for `user-id`, or nil when unavailable.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L50-L55)
---
## get-all-receipts
```clojure
(get-all-receipts client room-id)
```
Returns a Missionary flow of per-user receipt flows keyed by user id.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L57-L61)
---
## get-receipts-by-id
```clojure
(get-receipts-by-id client room-id user-id)
```
Returns a Missionary flow of receipts for `user-id` in `room-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L63-L68)
---
## get-power-level
```clojure
(get-power-level client room-id user-id)
```
Returns a Missionary flow of the current power-level view for `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L70-L75)
---
## can-kick-user
```clojure
(can-kick-user client room-id user-id)
```
Returns a Missionary flow that reports whether the current client can kick `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L77-L82)
---
## can-ban-user
```clojure
(can-ban-user client room-id user-id)
```
Returns a Missionary flow that reports whether the current client can ban `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L84-L89)
---
## can-unban-user
```clojure
(can-unban-user client room-id user-id)
```
Returns a Missionary flow that reports whether the current client can unban `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L91-L96)
---
## can-invite-user
```clojure
(can-invite-user client room-id user-id)
```
Returns a Missionary flow that reports whether the current client can invite `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L98-L103)
---
## can-invite
```clojure
(can-invite client room-id)
```
Returns a Missionary flow that reports whether the current client can invite users to `room-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L105-L109)
---
## can-redact-event
```clojure
(can-redact-event client room-id event-id)
```
Returns a Missionary flow that reports whether the current client can redact `event-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L111-L116)
---
## can-set-power-level-to-max
```clojure
(can-set-power-level-to-max client room-id user-id)
```
Returns a Missionary flow of the maximum power level the current client may assign to `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L118-L123)
---
## can-send-event
```clojure
(can-send-event client room-id event-class-or-content)
```
Returns a Missionary flow that reports whether the current client can send an event.
`event-class-or-content` may be either a room-event-content class or a
concrete room-event-content instance.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L125-L142)
---
## get-presence
```clojure
(get-presence client user-id)
```
Returns a Missionary flow of presence data for `user-id`.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L144-L148)
---
## get-account-data
```clojure
(get-account-data client event-content-class)
(get-account-data client event-content-class key)
```
Returns a Missionary flow of global account-data content.
When `key` is omitted, the empty-string state key is used.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/user.clj#L150-L161)
## ol.trixnity.verification
# ol.trixnity.verification
Verification state snapshots and active verification flows.
## Upstream Mapping
This namespace maps to Trixnity’s `VerificationService`.
The public wrappers here cover:
* the current active device verification and its relieved flow
* the current active user verifications and their relieved flow
* self-verification method discovery
Use [`ol.trixnity.key`](api/ol-trixnity-key.adoc) for trust and key-management APIs that often
accompany verification workflows.
## current-active-device-verification
```clojure
(current-active-device-verification client)
```
Returns the current active device verification, or nil when none is active.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/verification.clj#L23-L26)
---
## active-device-verification
```clojure
(active-device-verification client)
```
Returns a relieved Missionary flow of the current active device verification.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/verification.clj#L28-L32)
---
## current-active-user-verifications
```clojure
(current-active-user-verifications client)
```
Returns the current active user verifications as a vector.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/verification.clj#L34-L37)
---
## active-user-verifications
```clojure
(active-user-verifications client)
```
Returns a relieved Missionary flow of the current active user verifications.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/verification.clj#L39-L43)
---
## get-self-verification-methods
```clojure
(get-self-verification-methods client)
```
Returns a Missionary flow of available self-verification methods.
Upstream models this as a state machine that distinguishes unmet
preconditions, no-cross-signing-yet, already-cross-signed, and available
self-verification methods.
[source,window=_blank](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/verification.clj#L45-L52)
## Changelog
# Changelog
All notable changes to this project will be documented in this file.
This project uses [**Break Versioning**](https://www.taoensso.com/break-versioning).
## ++[++UNRELEASED++]++
## `v0.0.1` (2026-03-12)
Initial public release of `trixnity-clj`.
This release establishes the first published Clojure wrapper around Trixnity, with Missionary-based task and flow interop plus the sqlite4clj-backed repository module.
## Upstream API Coverage
# Upstream API Coverage
This page tracks `trixnity-clj` coverage of Trixnity’s API.
`Implemented` means there is a public wrapper under `ol.trixnity.*`.
For upstream `StateFlow` members, the Clojure surface usually appears as a
`current-*` accessor plus a Missionary flow.
`Not yet` means there is no public wrapper today.
## Client
### MatrixClient
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `userId` | `ol.trixnity.client/current-user-id` | Current snapshot only. |
| Not yet | `deviceId` | - | No public wrapper. |
| Not yet | `baseUrl` | - | No public wrapper. |
| Not yet | `identityKey` | - | No public wrapper. |
| Not yet | `signingKey` | - | No public wrapper. |
| Not yet | `di` | - | No public wrapper. |
| Not yet | `api` | - | Used internally by the bridge for some room endpoints, but not exposed publicly. |
| Implemented | `profile` | `ol.trixnity.client/current-profile`, `ol.trixnity.client/profile` | Current snapshot plus relieved flow. |
| Implemented | `serverData` | `ol.trixnity.client/current-server-data`, `ol.trixnity.client/server-data` | Current snapshot plus relieved flow. |
| Implemented | `syncState` | `ol.trixnity.client/current-sync-state`, `ol.trixnity.client/sync-state` | Current snapshot plus relieved flow. |
| Implemented | `initialSyncDone` | `ol.trixnity.client/current-initial-sync-done`, `ol.trixnity.client/initial-sync-done` | Current snapshot plus relieved flow. |
| Implemented | `loginState` | `ol.trixnity.client/current-login-state`, `ol.trixnity.client/login-state` | Current snapshot plus relieved flow. |
| Implemented | `started` | `ol.trixnity.client/current-started`, `ol.trixnity.client/started` | Current snapshot plus relieved flow. |
| Not yet | `logout()` | - | No public wrapper. |
| Not yet | `clearCache()` | - | No public wrapper. |
| Not yet | `clearMediaCache()` | - | No public wrapper. |
| Implemented | `startSync(...)` | `ol.trixnity.client/start-sync` | Uses upstream default presence. There is no public presence parameter. |
| Not yet | `syncOnce(...)` | - | No public wrapper. |
| Not yet | `syncOnce(..., runOnce)` | - | No public wrapper. |
| Implemented | `stopSync()` | `ol.trixnity.client/stop-sync` | - |
| Not yet | `cancelSync()` | - | No public wrapper. |
| Not yet | `setProfileField(...)` | - | No public wrapper. |
| Implemented | `closeSuspending()` | `ol.trixnity.client/close` | Public close path for the client lifecycle. |
### Additional Public APIs
* `ol.trixnity.client/open` wraps client creation or resume around the built-in
sqlite4clj repository configuration.
* `ol.trixnity.client/await-running` waits for `syncState` to reach `RUNNING`.
## Room
### RoomService
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `usersTyping` | `ol.trixnity.room/current-users-typing`, `ol.trixnity.room/users-typing` | Current snapshot plus relieved flow. |
| Implemented | `fillTimelineGaps(roomId, eventId, limit)` | `ol.trixnity.room/fill-timeline-gaps` | - |
| Implemented | `getTimelineEvent(roomId, eventId, config)` | `ol.trixnity.room/get-timeline-event` | Flow wrapper over one timeline event lookup. |
| Implemented | `getPreviousTimelineEvent(timelineEvent, config)` | `ol.trixnity.room/get-previous-timeline-event` | - |
| Implemented | `getNextTimelineEvent(timelineEvent, config)` | `ol.trixnity.room/get-next-timeline-event` | - |
| Implemented | `getLastTimelineEvent(roomId, config)` | `ol.trixnity.room/get-last-timeline-event` | Preserves the upstream nested-flow shape. |
| Implemented | `getTimelineEvents(roomId, startFrom, direction, config)` | `ol.trixnity.room/get-timeline-events`, `ol.trixnity.room/get-timeline-events-list` | `get-timeline-events-list` is a list-shaped convenience built on top of the same upstream timeline chain. |
| Implemented | `getLastTimelineEvents(roomId, config)` | `ol.trixnity.room/get-last-timeline-events`, `ol.trixnity.room/get-last-timeline-events-list` | `get-last-timeline-events-list` is a list-shaped convenience over the same upstream room-end traversal. |
| Implemented | `getTimelineEventsFromNowOn(...)` | `ol.trixnity.room/get-timeline-events-from-now-on` | Live timeline stream from the current sync position forward. |
| Implemented | `getTimelineEvents(response, decryptionTimeout)` | `ol.trixnity.room/get-timeline-events` | Covered by the sync-response arity. |
| Not yet | `getTimeline(...)` | - | The advanced mutable `Timeline` abstraction is not exposed yet. |
| Implemented | `getTimelineEventRelations(roomId, eventId, relationType)` | `ol.trixnity.room/get-timeline-event-relations` | - |
| Implemented | `sendMessage(roomId, wait, builder)` | `ol.trixnity.room/send-message`, `ol.trixnity.room/send-reaction` | `send-message` currently covers text messages and reply metadata. Reactions have a dedicated wrapper. |
| Implemented | `cancelSendMessage(roomId, transactionId)` | `ol.trixnity.room/cancel-send-message` | - |
| Implemented | `retrySendMessage(roomId, transactionId)` | `ol.trixnity.room/retry-send-message` | - |
| Implemented | `getAll()` | `ol.trixnity.room/get-all`, `ol.trixnity.room/get-all-flat` | `get-all-flat` flattens the nested upstream flow map. |
| Implemented | `getById(roomId)` | `ol.trixnity.room/get-by-id` | - |
| Implemented | `forgetRoom(roomId, force)` | `ol.trixnity.room/forget-room` | - |
| Implemented | `getAccountData(roomId, contentClass, key)` | `ol.trixnity.room/get-account-data` | - |
| Implemented | `getOutbox()` | `ol.trixnity.room/get-outbox`, `ol.trixnity.room/get-outbox-flat` | `get-outbox-flat` flattens the nested upstream outbox flows. |
| Implemented | `getOutbox(roomId)` | `ol.trixnity.room/get-outbox`, `ol.trixnity.room/get-outbox-flat` | Covered by room-scoped arities. |
| Implemented | `getOutbox(roomId, transactionId)` | `ol.trixnity.room/get-outbox` | Covered by the transaction-id arity. |
| Implemented | `getState(roomId, contentClass, stateKey)` | `ol.trixnity.room/get-state` | - |
| Implemented | `getAllState(roomId, contentClass)` | `ol.trixnity.room/get-all-state` | - |
### Additional Public APIs
| Status | Upstream API | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `MatrixClientServerApiClient.room.createRoom(...)` | `ol.trixnity.room/create-room` | Exposed directly through `client.api.room`, not through `RoomService`. |
| Implemented | `MatrixClientServerApiClient.room.inviteUser(...)` | `ol.trixnity.room/invite-user` | Exposed directly through `client.api.room`, not through `RoomService`. |
| Implemented | `MatrixClientServerApiClient.room.joinRoom(...)` | `ol.trixnity.room/join-room` | Exposed directly through `client.api.room`, not through `RoomService`. |
| Implemented | `de.connect2x.trixnity.client.room.getTimelineEventsAround(...)` | `ol.trixnity.room/get-timeline-events-around` | Centers a list-shaped view around one timeline event. |
## Timeline
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Not yet | `state` | - | No public wrapper for `TimelineState`. |
| Not yet | `init(...)` | - | No public wrapper. |
| Not yet | `loadBefore(...)` | - | No public wrapper. |
| Not yet | `loadAfter(...)` | - | No public wrapper. |
| Not yet | `dropBefore(...)` | - | No public wrapper. |
| Not yet | `dropAfter(...)` | - | No public wrapper. |
## UserService
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `loadMembers(roomId, wait)` | `ol.trixnity.user/load-members` | - |
| Implemented | `getAll(roomId)` | `ol.trixnity.user/get-all` | - |
| Implemented | `getById(roomId, userId)` | `ol.trixnity.user/get-by-id` | - |
| Implemented | `getAllReceipts(roomId)` | `ol.trixnity.user/get-all-receipts` | - |
| Implemented | `getReceiptsById(roomId, userId)` | `ol.trixnity.user/get-receipts-by-id` | - |
| Implemented | `getPowerLevel(roomId, userId)` | `ol.trixnity.user/get-power-level` | - |
| Not yet | `getPowerLevel(roomId, createEvent, powerLevels)` | - | Pure helper overload is not wrapped. |
| Implemented | `canKickUser(roomId, userId)` | `ol.trixnity.user/can-kick-user` | - |
| Implemented | `canBanUser(roomId, userId)` | `ol.trixnity.user/can-ban-user` | - |
| Implemented | `canUnbanUser(roomId, userId)` | `ol.trixnity.user/can-unban-user` | - |
| Implemented | `canInviteUser(roomId, userId)` | `ol.trixnity.user/can-invite-user` | - |
| Implemented | `canInvite(roomId)` | `ol.trixnity.user/can-invite` | - |
| Implemented | `canRedactEvent(roomId, eventId)` | `ol.trixnity.user/can-redact-event` | - |
| Implemented | `canSetPowerLevelToMax(roomId, userId)` | `ol.trixnity.user/can-set-power-level-to-max` | - |
| Implemented | `canSendEvent(roomId, KClass)` | `ol.trixnity.user/can-send-event` | Covered by the class-taking arity. |
| Implemented | `canSendEvent(roomId, RoomEventContent)` | `ol.trixnity.user/can-send-event` | Covered by the content-taking arity. |
| Implemented | `getPresence(userId)` | `ol.trixnity.user/get-presence` | - |
| Implemented | `getAccountData(contentClass, key)` | `ol.trixnity.user/get-account-data` | - |
## NotificationService
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `getNotifications(decryptionTimeout, syncResponseBufferSize)` | `ol.trixnity.notification/get-notifications` | Deprecated upstream path. Still exposed as a deprecated Clojure wrapper. |
| Implemented | `getNotifications(response, decryptionTimeout)` | `ol.trixnity.notification/get-notifications` | Covered by the response-taking deprecated arity. |
| Implemented | `getAll()` | `ol.trixnity.notification/get-all`, `ol.trixnity.notification/get-all-flat` | `get-all-flat` flattens the nested upstream flows. |
| Implemented | `getById(id)` | `ol.trixnity.notification/get-by-id` | - |
| Implemented | `getCount()` | `ol.trixnity.notification/get-count` | Covered by the one-argument arity. |
| Implemented | `getCount(roomId)` | `ol.trixnity.notification/get-count` | Covered by the room-scoped arity. |
| Implemented | `isUnread(roomId)` | `ol.trixnity.notification/is-unread` | - |
| Implemented | `dismiss(id)` | `ol.trixnity.notification/dismiss` | - |
| Implemented | `dismissAll()` | `ol.trixnity.notification/dismiss-all` | - |
| Implemented | `getAllUpdates()` | `ol.trixnity.notification/get-all-updates` | - |
| Not yet | `onPush(roomId, eventId)` | - | No public wrapper. |
| Not yet | `processPending()` | - | No public wrapper. |
## VerificationService
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `activeDeviceVerification` | `ol.trixnity.verification/current-active-device-verification`, `ol.trixnity.verification/active-device-verification` | Current snapshot plus relieved flow. |
| Implemented | `activeUserVerifications` | `ol.trixnity.verification/current-active-user-verifications`, `ol.trixnity.verification/active-user-verifications` | Current snapshot plus relieved flow. |
| Not yet | `createDeviceVerificationRequest(...)` | - | No public wrapper. |
| Not yet | `createUserVerificationRequest(...)` | - | No public wrapper. |
| Implemented | `getSelfVerificationMethods()` | `ol.trixnity.verification/get-self-verification-methods` | - |
| Not yet | `getActiveUserVerification(timelineEvent)` | - | No public wrapper. |
| Not yet | `getActiveUserVerification(roomId, eventId)` | - | No public wrapper. |
## KeyService
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `bootstrapRunning` | `ol.trixnity.key/current-bootstrap-running`, `ol.trixnity.key/bootstrap-running` | Current snapshot plus relieved flow. |
| Not yet | `bootstrapCrossSigning(secretKey, secretKeyEvent)` | - | No public wrapper. |
| Not yet | `bootstrapCrossSigning()` | - | No public wrapper. |
| Not yet | `bootstrapCrossSigningFromPassphrase(passphrase)` | - | No public wrapper. |
| Implemented | `getTrustLevel(userId, deviceId)` | `ol.trixnity.key/get-trust-level` | Covered by the three-argument arity when the first id is a user id. |
| Implemented | `getTrustLevel(roomId, eventId)` | `ol.trixnity.key/get-trust-level` | Covered by the three-argument arity when the first id is a room id. |
| Implemented | `getTrustLevel(userId)` | `ol.trixnity.key/get-trust-level` | Covered by the two-argument arity. |
| Implemented | `getDeviceKeys(userId)` | `ol.trixnity.key/get-device-keys` | - |
| Implemented | `getCrossSigningKeys(userId)` | `ol.trixnity.key/get-cross-signing-keys` | - |
## KeyBackupService
| Status | Upstream member | `trixnity-clj` surface | Notes |
| --- | --- | --- | --- |
| Implemented | `version` | `ol.trixnity.key/current-backup-version`, `ol.trixnity.key/backup-version` | Current snapshot plus relieved flow. |
| Not yet | `loadMegolmSession(roomId, sessionId)` | - | No public wrapper. |
| Not yet | `keyBackupCanBeTrusted(version, recoveryKey)` | - | No public wrapper. |
| Not yet | `bootstrapRoomKeyBackup(...)` | - | No public wrapper. |
## ol.trixnity
# ol.trixnity
> A Clojure adapter for Trixnity, a Matrix SDK. Write Matrix apps and bots in Clojure.


[Trixnity](https://trixnity.connect2x.de/) is a Kotlin Multiplatform library for building Matrix applications. It covers the full Matrix client-server API surface, including room management, sync, and end-to-end encryption. Despite flying under the radar, Trixnity already powers a large chunk of Germany’s TI-Messenger healthcare infrastructure, reaching tens of millions of potential users.
Trixnity targets the JVM, which means Clojure can call into it directly, but the interop is still rough. Nearly every interesting API is either a Kotlin suspend function or returns a `Flow` or `StateFlow`, so even basic client work quickly turns into coroutine plumbing: scopes, dispatchers, cancellation, and stream observation.
The flow-heavy parts are especially awkward because they are long-lived Kotlin stream types, not something Clojure can consume idiomatically. So things like sync state, timeline updates, and other live observations need extra translation. Kotlin’s compiler also mangles names in the compiled bytecode, which makes direct interop harder when reading the upstream docs.
`trixnity-clj` wraps Trixnity with a small Kotlin bridge and exposes the concurrency model using [Missionary](https://github.com/leonoel/missionary/). Kotlin suspend functions become Missionary tasks. Kotlin `Flow` and `StateFlow` surfaces become Missionary flows. `StateFlow` properties are exposed as a synchronous `current-*` getter plus a relieved Missionary flow.
Project status: **[Experimental](https://docs.outskirtslabs.com/open-source-vital-signs#experimental)**.
## Repository Backend
In Trixnity, a repository module is the persistence backend for all the client state it needs to keep around: sync tokens, rooms, timelines, outbox entries, encryption keys, notifications, and the rest of the Matrix baggage.
Upstream ships several of these across platforms. If you want durable storage on the JVM, the stock option is the [Exposed repository module](https://gitlab.com/trixnity/trixnity/-/tree/main/trixnity-client/trixnity-client-repository-exposed), which means JetBrains’s ORM Exposed layered on top of JDBC.
Maybe that sparks joy for somebody. It does not spark joy for me.
For the kind of bots and small clients I am building, running SQLite through JDBC makes write serialization harder than it should be, which means more `busy_timeout` pain.
And god forbid anyone suggest dragging Hikari into that mess.
So this library includes its own repository implementation backed by [sqlite4clj](https://github.com/andersmurphy/sqlite4clj), and that is the default setup.
The storage layer is just plain SQLite, explicit SQL, and a lot less nonsense.
Shout out to Anders Murphy for building [sqlite4clj](https://github.com/andersmurphy/sqlite4clj), and making the SQLite story in Clojure 100% less deranged.
## Example Usage
Use [`ol.trixnity.repo`](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/repo.clj) to configure the built-in sqlite4clj repository, then open a client with [`ol.trixnity.client/open`](https://github.com/outskirtslabs/trixnity-clj/blob/main/src/clj/ol/trixnity/client.clj):
```clojure
(ns my.bot
(:require
[missionary.core :as m]
[ol.trixnity.client :as client]
[ol.trixnity.event :as event]
[ol.trixnity.repo :as repo]
[ol.trixnity.room :as room]
[ol.trixnity.room.message :as msg]
[ol.trixnity.schemas :as mx])
(:import
[java.time Duration]))
(defn run-bot []
(let [client (m/? (client/open
(merge
{::mx/homeserver-url "https://matrix.example.org"
::mx/username "bot"
::mx/password "secret"}
(repo/sqlite4clj-config
{:database-path "./var/trixnity.sqlite"
:media-path "./var/media"}))))]
(m/? (client/start-sync client))
(m/? (client/await-running client
{::mx/timeout (Duration/ofSeconds 30)}))
(future
(m/? (m/reduce
(fn [_ ev]
(when (event/text? ev)
(m/? (room/send-message
client
(event/room-id ev)
(-> (msg/text "pong")
(msg/reply-to ev))
{::mx/timeout (Duration/ofSeconds 5)})))
nil)
nil
(room/get-timeline-events-from-now-on
client
{::mx/decryption-timeout (Duration/ofSeconds 8)}))))
{:client client}))
```
If you want a different repository implementation, construct a `MatrixClient` yourself and pass it to `client/open` as `::mx/client`.
## Documentation
* [Docs](https://docs.outskirtslabs.com/ol.trixnity/next/)
* [API Reference](https://docs.outskirtslabs.com/ol.trixnity/next/api)
* [Support via GitHub Issues](https://github.com/outskirtslabs/trixnity-clj/issues)
## Examples
Several examples are available in [examples](examples/):
* [basic-bot](examples/basic-bot/) - A minimal end-to-end bot that opens a client, starts sync, joins invited rooms, and replies to live timeline events.
## License
Copyright (C) 2026 Casey Link
Distributed under the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) just like Trixnity itself.
## Security policy
# Security policy
## Advisories
All security advisories for `trixnity-clj` will be posted [on GitHub](https://github.com/outskirtslabs/trixnity-clj/security/advisories).
## Reporting a vulnerability
Please report possible security vulnerabilities [via GitHub](https://github.com/outskirtslabs/trixnity-clj/security/advisories), or by emailing me at `casey@outskirtslabs.com`. You may encrypt email with [my public PGP key](https://casey.link/pgp.asc).
For the organization-wide security policy, see [Outskirts Labs Security Policy](https://docs.outskirtslabs.com/security-policy).
Thank you!
[Casey Link](https://casey.link)
## Open Source Vital Signs
# Open Source Vital Signs
This page outlines the taxonomy I employ to describe the maturity levels of my
various open source projects. By understanding a project’s current status, you
can make informed decisions about its suitability for your needs, the level of
engagement you should expect from me, and whether it’s on track for long-term
support.
If you rely on a project for critical use cases, prefer those marked **Stable**.
If you enjoy shaping software early, look for **Experimental** and **Maturing** projects.
## Experimental
_Beware of moving parts and sharp edges._
Experimental projects are early-stage explorations. I am validating ideas, APIs, and architecture. Breaking changes are likely. Documentation may be incomplete. The goal is learning and iteration, not stability.
Contribute early and often. Experimental projects are where your input can make the biggest difference.
| | |
| --- | --- |
| Activity | Supported |
| New features | Yes |
| Feature requests | Yes |
| Bug and security fixes | No |
| Bug reports | Yes |
| New releases | Maybe? |
## Maturing
_Actively developed and moving toward stability._
Maturing projects have graduated from my mad science lab and taken up residence
in the "maybe someday this could be useful" wing of my brain. They’ve proven
themselves useful, are under active development, and are moving closer to
stability. APIs may change, but those changes are more deliberate now.
Documentation is improving, and I try not to break things quite as much.
Backward compatibility is considered, though it can’t be guaranteed.
If you’re cool with the idea of keeping up with the project’s evolution (and
occasional minor breaking changes), maturing projects are worth a closer look.
They might even be suitable for early production use.
| | |
| --- | --- |
| Activity | Supported |
| New features | Limited |
| Feature requests | Limited |
| Bug and security fixes | Yes |
| Bug reports | Yes |
| New releases | Yes |
## Stable
_Production-ready and well behaved._
Stable projects have left their rebellious teenager phase behind. They’ve found steady jobs, bought houses, and aren’t looking for new hobbies.
The scope is defined, the API is predictable, and documentation is complete enough that you should be able to use them independently.
New features are rare, not because these projects are neglected, but because they do what they set out to do.
If they had driver’s licenses, I’d feel comfortable letting you loan them out for road trips.
Use Stable projects in production with confidence. Major changes will be rare and clearly communicated.
| | |
| --- | --- |
| Activity | Supported |
| New features | Limited |
| Feature requests | No |
| Bug and security fixes | Yes |
| Bug reports | Yes |
| New releases | Yes |
## Retired
_Inactive and out of service._
Retired projects have left the building for good. They may depend on outdated technologies, have been replaced by newer projects, or no longer align with my interests.
If you’re inclined, feel free to fork them and keep the dream alive; but I do not plan to review issues, accept pull requests, or publish new releases.
If you’re inclined, feel free to keep the dream alive on your own terms (just keep the license intact).
| | |
| --- | --- |
| Activity | Supported |
| New features | No |
| Feature requests | No |
| Bug and security fixes | No |
| Bug reports | No |
| New releases | No |
## Static
_Code with a "do not feed" sign._
Static projects are like exhibits at the zoo: they’re here to be observed and appreciated, not taken home.
These are snippets of code published alongside research papers or blog posts, demonstration examples, or educational materials.
Don’t expect any updates; I won’t be making any, because the purpose of these projects is to serve as a reference, not to build your production system.
## Recent Releases
# Recent Releases
Full release feed across all tracked projects.
| Date | Library | Version |
| --- | --- | --- |
| 2026-03-12 | [ol.dirs](../ol.dirs/0.1/) | `v0.1.0` |
| 2026-03-12 | [ol.sops](../ol.sops/0.1/) | `v0.1.0` |
| 2026-03-09 | [ol.trixnity](../ol.trixnity/next/) | `experimental` |
| 2025-12-18 | [nixos-hetzner](../nixos-hetzner/next/) | `static` |
| 2025-12-18 | [nixos-hetzner-demo](../nixos-hetzner-demo/next/) | `static` |
| 2025-10-28 | [ol.clave](../ol.clave/next/) | `experimental` |
| 2025-10-20 | [h2o-zig](../h2o-zig/next/) | `maturing` |
| 2025-09-08 | [ol.sfv](../ol.sfv/0.1/) | `v0.1.0` |
| 2025-08-18 | [ol.client-ip](../ol.client-ip/0.1/) | `v0.1.0` |
| 2025-04-22 | [datahike-sqlite](../datahike-sqlite/next/) | `experimental` |
| 2025-04-14 | [datastar-expressions](../datastar-expressions/next/) | `experimental` |
## Security Policy
# Security Policy
## Report a Security Issue
I take security reports seriously and will give them prompt attention.
Security issues should be reported privately to `casey@outskirtslabs.com`, please use the string `OSS-SEC` somewhere in the subject line.
You may encrypt emails with [my public PGP key](https://casey.link/pgp.asc).
_Please_ do not file public issues for security vulnerabilities.
## Security Advisories
Remediation of security vulnerabilities for [maturing](open-source-vital-signs.adoc#maturing) and [stable](open-source-vital-signs.adoc#stable) projects is prioritized.
I endeavor to coordinate remediation with third-party stakeholders and am committed to transparency in the disclosure process.
Security issues are announced via Release notes and Security Advisories on the affected project’s repository, as well as at [docs.outskirtslabs.com](https://docs.outskirtslabs.com) on a best-effort basis.
## Support
# Support
If these projects bring you joy or save you time or money, supporting the work helps me keep them healthy and moving forward (it also helps keep me healthy and moving forward). There are two straightforward ways to do that.
## You are human person
If you are an individual developer or hobbyist, your sponsorship helps fund ongoing maintenance and releases.
Liberapay is my preferred option, because it has lower fees, supports my family, and doesn’t enable a monstrous mega-corporation hellbent on world domination.
But I won’t judge if you use GitHub Sponsors; it works too.
Even small recurring or one-time sponsorships make a real difference, it signals to the community that there is something of value here.
## You are a legal entity
If your company depends on one or more Outskirts Labs projects, I offer paid consulting engagements.
Typical engagements include:
* architecture and implementation guidance
* integrations and custom feature/bug development
* production hardening and reliability work
* team enablement and long-term maintenance planning
To discuss scope, timeline, and budget, email mailto:casey@outskirtslabs.com[casey@outskirtslabs.com].
(p.s. All engagements come with a EU-friendly VAT invoice that will make your bookeeper happy.)