···11+-- Shared reverse index schema
22+33+CREATE TABLE IF NOT EXISTS zone_index (
44+ zone TEXT NOT NULL,
55+ did TEXT NOT NULL,
66+ verified INTEGER NOT NULL DEFAULT 0,
77+ first_seen INTEGER NOT NULL,
88+ last_verified INTEGER,
99+ PRIMARY KEY (zone, did)
1010+);
1111+1212+CREATE INDEX IF NOT EXISTS idx_zone_index_did
1313+ ON zone_index(did);
+22
migrations/user/001_init.sql
···11+-- Per-DID database schema
22+33+CREATE TABLE IF NOT EXISTS records (
44+ rkey TEXT PRIMARY KEY,
55+ domain TEXT NOT NULL,
66+ record_type TEXT NOT NULL,
77+ data TEXT NOT NULL, -- JSON-encoded full DnsRecord (domain + ttl + record)
88+ created_at INTEGER NOT NULL,
99+ updated_at INTEGER NOT NULL
1010+);
1111+1212+CREATE TABLE IF NOT EXISTS zones (
1313+ rkey TEXT PRIMARY KEY,
1414+ domain TEXT NOT NULL UNIQUE,
1515+ created_at INTEGER NOT NULL
1616+);
1717+1818+CREATE INDEX IF NOT EXISTS idx_records_domain
1919+ ON records(domain);
2020+2121+CREATE INDEX IF NOT EXISTS idx_records_domain_type
2222+ ON records(domain, record_type);
···11+use std::fs;
22+use std::path::PathBuf;
33+44+use esquema_codegen::genapi;
55+66+fn main() {
77+ let lex_dir = PathBuf::from("../lexicons");
88+ let out_dir = PathBuf::from("src");
99+1010+ fs::create_dir_all(&out_dir.join("lexicons")).unwrap();
1111+1212+ println!("cargo:rerun-if-changed=../lexicons/");
1313+1414+ let module_name = Some("lexicons".to_string());
1515+ let _ = genapi(&lex_dir, &out_dir, &module_name).unwrap();
1616+}
+304
onis-common/src/config.rs
···11+use std::net::IpAddr;
22+use std::path::Path;
33+44+use serde::Deserialize;
55+use thiserror::Error;
66+77+#[derive(Debug, Error)]
88+pub enum ConfigError {
99+ #[error("io error: {0}")]
1010+ Io(#[from] std::io::Error),
1111+ #[error("toml parse error: {0}")]
1212+ Toml(#[from] toml::de::Error),
1313+}
1414+1515+/// Top-level config covering all onis services.
1616+///
1717+/// Load from a TOML file with [`OnisConfig::load`]. Every field has a default,
1818+/// so an empty (or missing) file produces a usable config.
1919+#[derive(Debug, Deserialize)]
2020+#[serde(default)]
2121+pub struct OnisConfig {
2222+ /// Configuration for the appview service.
2323+ pub appview: AppviewConfig,
2424+ /// Configuration for the DNS server.
2525+ pub dns: DnsConfig,
2626+ /// Configuration for the verification service.
2727+ pub verify: VerifyConfig,
2828+}
2929+3030+impl OnisConfig {
3131+ /// Load config from `ONIS_CONFIG` env var path, or `onis.toml` in the
3232+ /// current directory. Returns defaults if the file does not exist.
3333+ pub fn load() -> Result<Self, ConfigError> {
3434+ let path = std::env::var("ONIS_CONFIG").unwrap_or_else(|_| "onis.toml".to_string());
3535+ let path = Path::new(&path);
3636+3737+ if path.exists() {
3838+ let content = std::fs::read_to_string(path)?;
3939+ Ok(toml::from_str(&content)?)
4040+ } else {
4141+ Ok(Self::default())
4242+ }
4343+ }
4444+}
4545+4646+impl Default for OnisConfig {
4747+ fn default() -> Self {
4848+ Self {
4949+ appview: AppviewConfig::default(),
5050+ dns: DnsConfig::default(),
5151+ verify: VerifyConfig::default(),
5252+ }
5353+ }
5454+}
5555+5656+#[derive(Debug, Deserialize)]
5757+#[serde(default)]
5858+pub struct AppviewConfig {
5959+ /// Address and port for the appview HTTP server.
6060+ pub bind: String,
6161+ /// WebSocket URL for the TAP firehose.
6262+ pub tap_url: String,
6363+ /// Whether to acknowledge TAP messages.
6464+ pub tap_acks: bool,
6565+ /// Seconds to wait before reconnecting after a TAP connection error.
6666+ pub tap_reconnect_delay: u64,
6767+ /// Path to the shared zone index SQLite database.
6868+ pub index_path: String,
6969+ /// Directory for per-DID SQLite databases.
7070+ pub db_dir: String,
7171+ /// Database pool configuration.
7272+ pub database: DatabaseConfig,
7373+}
7474+7575+impl Default for AppviewConfig {
7676+ fn default() -> Self {
7777+ Self {
7878+ bind: "0.0.0.0:3000".to_string(),
7979+ tap_url: "ws://localhost:2480/channel".to_string(),
8080+ tap_acks: true,
8181+ tap_reconnect_delay: 5,
8282+ index_path: "./data/index.db".to_string(),
8383+ db_dir: "./data/dbs".to_string(),
8484+ database: DatabaseConfig::default(),
8585+ }
8686+ }
8787+}
8888+8989+#[derive(Debug, Clone, Deserialize)]
9090+#[serde(default)]
9191+pub struct DatabaseConfig {
9292+ /// Seconds to wait when the database is locked.
9393+ pub busy_timeout: u64,
9494+ /// Max connections for per-user database pools.
9595+ pub user_max_connections: u32,
9696+ /// Max connections for the shared index database pool.
9797+ pub index_max_connections: u32,
9898+}
9999+100100+impl Default for DatabaseConfig {
101101+ fn default() -> Self {
102102+ Self {
103103+ busy_timeout: 5,
104104+ user_max_connections: 5,
105105+ index_max_connections: 10,
106106+ }
107107+ }
108108+}
109109+110110+#[derive(Debug, Deserialize)]
111111+#[serde(default)]
112112+pub struct DnsConfig {
113113+ /// URL of the appview API.
114114+ pub appview_url: String,
115115+ /// Address for the DNS server to listen on.
116116+ pub bind: String,
117117+ /// Port for the DNS server.
118118+ pub port: u16,
119119+ /// Seconds before a TCP connection times out.
120120+ pub tcp_timeout: u64,
121121+ /// Minimum TTL enforced on all DNS responses.
122122+ pub ttl_floor: u32,
123123+ /// Log a warning for queries slower than this (milliseconds).
124124+ pub slow_query_threshold_ms: u64,
125125+ /// SOA record defaults for zones without a user-published SOA.
126126+ pub soa: SoaConfig,
127127+ /// NS records to serve for all zones (fully qualified, trailing dot).
128128+ pub ns: Vec<String>,
129129+ /// Bind address for the metrics HTTP server (e.g. "0.0.0.0:9100").
130130+ pub metrics_bind: String,
131131+}
132132+133133+impl Default for DnsConfig {
134134+ fn default() -> Self {
135135+ Self {
136136+ appview_url: "http://localhost:3000".to_string(),
137137+ bind: "0.0.0.0".to_string(),
138138+ port: 5353,
139139+ tcp_timeout: 30,
140140+ ttl_floor: 60,
141141+ slow_query_threshold_ms: 50,
142142+ soa: SoaConfig::default(),
143143+ ns: vec![
144144+ "ns1.kiri.systems.".to_string(),
145145+ "ns2.kiri.systems.".to_string(),
146146+ ],
147147+ metrics_bind: "0.0.0.0:9100".to_string(),
148148+ }
149149+ }
150150+}
151151+152152+#[derive(Debug, Deserialize)]
153153+#[serde(default)]
154154+pub struct SoaConfig {
155155+ /// SOA record TTL in seconds.
156156+ pub ttl: u32,
157157+ /// SOA refresh interval in seconds.
158158+ pub refresh: i32,
159159+ /// SOA retry interval in seconds.
160160+ pub retry: i32,
161161+ /// SOA expire interval in seconds.
162162+ pub expire: i32,
163163+ /// SOA minimum (negative cache) TTL in seconds.
164164+ pub minimum: u32,
165165+ /// SOA MNAME (primary nameserver, fully qualified).
166166+ pub mname: String,
167167+ /// SOA RNAME (admin email in DNS format, fully qualified).
168168+ pub rname: String,
169169+}
170170+171171+impl Default for SoaConfig {
172172+ fn default() -> Self {
173173+ Self {
174174+ ttl: 3600,
175175+ refresh: 3600,
176176+ retry: 900,
177177+ expire: 604800,
178178+ minimum: 300,
179179+ mname: "ns1.kiri.systems.".to_string(),
180180+ rname: "admin.kiri.systems.".to_string(),
181181+ }
182182+ }
183183+}
184184+185185+#[derive(Debug, Deserialize)]
186186+#[serde(default)]
187187+pub struct VerifyConfig {
188188+ /// Onis appview to call.
189189+ pub appview_url: String,
190190+ /// Address to start listening on for api.
191191+ pub bind: String,
192192+ /// Port used for api.
193193+ pub port: u16,
194194+ /// Seconds between scheduled verification runs.
195195+ pub check_interval: u64,
196196+ /// Seconds a zone must be stale before rechecking.
197197+ pub recheck_interval: i64,
198198+ /// Expected NS records that indicate correct delegation.
199199+ pub expected_ns: Vec<String>,
200200+ /// Optional custom resolver IP addresses.
201201+ pub nameservers: Vec<String>,
202202+ /// Port used when resolving against custom nameservers.
203203+ pub dns_port: u16,
204204+}
205205+206206+impl Default for VerifyConfig {
207207+ fn default() -> Self {
208208+ Self {
209209+ appview_url: "http://localhost:3000".to_string(),
210210+ bind: "0.0.0.0".to_string(),
211211+ port: 3001,
212212+ check_interval: 60,
213213+ recheck_interval: 3600,
214214+ expected_ns: vec![
215215+ "curitiba.ns.porkbun.com".to_string(),
216216+ "maceio.ns.porkbun.com".to_string(),
217217+ ],
218218+ nameservers: vec![],
219219+ dns_port: 53,
220220+ }
221221+ }
222222+}
223223+224224+impl VerifyConfig {
225225+ /// Parse the nameservers list into IP addresses.
226226+ /// Returns None if the list is empty.
227227+ pub fn parse_nameservers(&self) -> Result<Option<Vec<IpAddr>>, std::net::AddrParseError> {
228228+ if self.nameservers.is_empty() {
229229+ return Ok(None);
230230+ }
231231+ let addrs: Result<Vec<IpAddr>, _> = self
232232+ .nameservers
233233+ .iter()
234234+ .map(|s| s.parse())
235235+ .collect();
236236+ addrs.map(Some)
237237+ }
238238+}
239239+240240+#[cfg(test)]
241241+#[allow(clippy::unwrap_used)]
242242+mod tests {
243243+ use super::*;
244244+245245+ #[test]
246246+ fn parse_nameservers_empty_returns_none() {
247247+ let config = VerifyConfig::default();
248248+ let result = config.parse_nameservers().unwrap();
249249+ assert_eq!(result, None);
250250+ }
251251+252252+ #[test]
253253+ fn parse_nameservers_valid_ipv4() {
254254+ let config = VerifyConfig {
255255+ nameservers: vec!["1.1.1.1".to_string(), "8.8.8.8".to_string()],
256256+ ..Default::default()
257257+ };
258258+ let result = config.parse_nameservers().unwrap().unwrap();
259259+ assert_eq!(result.len(), 2);
260260+ assert_eq!(result, vec![
261261+ "1.1.1.1".parse::<IpAddr>().unwrap(),
262262+ "8.8.8.8".parse::<IpAddr>().unwrap(),
263263+ ]);
264264+ }
265265+266266+ #[test]
267267+ fn parse_nameservers_valid_ipv6() {
268268+ let config = VerifyConfig {
269269+ nameservers: vec!["2001:4860:4860::8888".to_string()],
270270+ ..Default::default()
271271+ };
272272+ let result = config.parse_nameservers().unwrap().unwrap();
273273+ assert_eq!(result.len(), 1);
274274+ assert_eq!(result, vec!["2001:4860:4860::8888".parse::<IpAddr>().unwrap()]);
275275+ }
276276+277277+ #[test]
278278+ fn parse_nameservers_mixed_v4_v6() {
279279+ let config = VerifyConfig {
280280+ nameservers: vec!["1.1.1.1".to_string(), "::1".to_string()],
281281+ ..Default::default()
282282+ };
283283+ let result = config.parse_nameservers().unwrap().unwrap();
284284+ assert_eq!(result.len(), 2);
285285+ }
286286+287287+ #[test]
288288+ fn parse_nameservers_invalid_returns_err() {
289289+ let config = VerifyConfig {
290290+ nameservers: vec!["not-an-ip".to_string()],
291291+ ..Default::default()
292292+ };
293293+ assert!(config.parse_nameservers().is_err());
294294+ }
295295+296296+ #[test]
297297+ fn parse_nameservers_one_invalid_fails_all() {
298298+ let config = VerifyConfig {
299299+ nameservers: vec!["1.1.1.1".to_string(), "bad".to_string()],
300300+ ..Default::default()
301301+ };
302302+ assert!(config.parse_nameservers().is_err());
303303+ }
304304+}
+183
onis-common/src/db.rs
···11+//! SQLite database helpers for onis using sqlx.
22+//!
33+//! Two database types:
44+//! - Per-DID databases: one per user, stores their DNS records
55+//! - Reverse index: shared database mapping domains → DIDs + verification status
66+//!
77+//! Migrations live in:
88+//! migrations/user/ — per-DID database migrations
99+//! migrations/index/ — reverse index migrations
1010+1111+use std::path::Path;
1212+1313+use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
1414+use thiserror::Error;
1515+1616+use crate::config::DatabaseConfig;
1717+1818+#[derive(Debug, Error)]
1919+pub enum DbError {
2020+ #[error("sqlx error: {0}")]
2121+ Sqlx(#[from] sqlx::Error),
2222+ #[error("migrate error: {0}")]
2323+ Migrate(#[from] sqlx::migrate::MigrateError),
2424+ #[error("io error: {0}")]
2525+ Io(#[from] std::io::Error),
2626+}
2727+2828+/// Opens (or creates) a per-DID SQLite database.
2929+///
3030+/// Runs migrations from `migrations/user/` on first open.
3131+pub async fn open_user_db(path: &Path, db_config: &DatabaseConfig) -> Result<SqlitePool, DbError> {
3232+ if let Some(parent) = path.parent() {
3333+ tokio::fs::create_dir_all(parent).await?;
3434+ }
3535+3636+ let opts = SqliteConnectOptions::new()
3737+ .filename(path)
3838+ .create_if_missing(true)
3939+ .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
4040+ .busy_timeout(std::time::Duration::from_secs(db_config.busy_timeout));
4141+4242+ let pool = SqlitePoolOptions::new()
4343+ .max_connections(db_config.user_max_connections)
4444+ .connect_with(opts)
4545+ .await?;
4646+4747+ let migrator = sqlx::migrate!("../migrations/user");
4848+ tracing::info!("migrations to run: {}", migrator.migrations.len());
4949+ migrator.run(&pool).await?;
5050+5151+ Ok(pool)
5252+}
5353+5454+/// Opens (or creates) the shared reverse index database.
5555+///
5656+/// Runs migrations from `migrations/index/` on first open.
5757+pub async fn open_index_db(path: &Path, db_config: &DatabaseConfig) -> Result<SqlitePool, DbError> {
5858+ if let Some(parent) = path.parent() {
5959+ tokio::fs::create_dir_all(parent).await?;
6060+ }
6161+6262+ let opts = SqliteConnectOptions::new()
6363+ .filename(path)
6464+ .create_if_missing(true)
6565+ .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
6666+ .busy_timeout(std::time::Duration::from_secs(db_config.busy_timeout));
6767+6868+ let pool = SqlitePoolOptions::new()
6969+ .max_connections(db_config.index_max_connections)
7070+ .connect_with(opts)
7171+ .await?;
7272+7373+ sqlx::migrate!("../migrations/index").run(&pool).await?;
7474+7575+ Ok(pool)
7676+}
7777+7878+/// Converts a DID to a filesystem-safe path for its database.
7979+///
8080+/// e.g. `did:plc:adtzorbhmmjbzxsl2y4vqlqs` → `{base}/ad/tz/did_plc_adtzorbhmmjbzxsl2y4vqlqs.db`
8181+/// e.g. `did:web:vim.sh` → `{base}/vi/m./did_web_vim.sh.db`
8282+pub fn did_to_db_path(base: &Path, did: &str) -> std::path::PathBuf {
8383+ let safe = did.replace(':', "_");
8484+8585+ // use first 4 chars after "did_(plc|web)_" for sharding
8686+ // XXX: this will break if another did method is added
8787+ // thats method is longer than 3 characters.
8888+ let shard = if safe.len() > 8 {
8989+ &safe[8..]
9090+ } else {
9191+ &safe
9292+ };
9393+9494+ let (a, b) = if shard.len() >= 4 {
9595+ (&shard[..2], &shard[2..4])
9696+ } else {
9797+ ("xx", "xx")
9898+ };
9999+ base.join(a).join(b).join(format!("{safe}.db"))
100100+}
101101+102102+#[cfg(test)]
103103+mod tests {
104104+ use super::*;
105105+ use std::path::PathBuf;
106106+107107+ #[test]
108108+ fn did_plc_standard() {
109109+ let base = PathBuf::from("/data/dbs");
110110+ let path = did_to_db_path(&base, "did:plc:adtzorbhmmjbzxsl2y4vqlqs");
111111+ assert_eq!(
112112+ path,
113113+ PathBuf::from("/data/dbs/ad/tz/did_plc_adtzorbhmmjbzxsl2y4vqlqs.db")
114114+ );
115115+ }
116116+117117+ #[test]
118118+ fn did_web_domain() {
119119+ let base = PathBuf::from("/data/dbs");
120120+ let path = did_to_db_path(&base, "did:web:example.com");
121121+ assert_eq!(
122122+ path,
123123+ PathBuf::from("/data/dbs/ex/am/did_web_example.com.db")
124124+ );
125125+ }
126126+127127+ #[test]
128128+ fn did_web_short_domain() {
129129+ let base = PathBuf::from("/data/dbs");
130130+ let path = did_to_db_path(&base, "did:web:vim.sh");
131131+ assert_eq!(
132132+ path,
133133+ PathBuf::from("/data/dbs/vi/m./did_web_vim.sh.db")
134134+ );
135135+ }
136136+137137+ #[test]
138138+ fn did_web_subdomain() {
139139+ let base = PathBuf::from("/data/dbs");
140140+ let path = did_to_db_path(&base, "did:web:sub.example.com");
141141+ assert_eq!(
142142+ path,
143143+ PathBuf::from("/data/dbs/su/b./did_web_sub.example.com.db")
144144+ );
145145+ }
146146+147147+ #[test]
148148+ fn did_web_very_short_falls_back() {
149149+ let base = PathBuf::from("/data/dbs");
150150+ let path = did_to_db_path(&base, "did:web:a.b");
151151+ assert_eq!(
152152+ path,
153153+ PathBuf::from("/data/dbs/xx/xx/did_web_a.b.db")
154154+ );
155155+ }
156156+157157+ #[test]
158158+ fn did_plc_short_falls_back() {
159159+ let base = PathBuf::from("/data/dbs");
160160+ let path = did_to_db_path(&base, "did:plc:abc");
161161+ assert_eq!(
162162+ path,
163163+ PathBuf::from("/data/dbs/xx/xx/did_plc_abc.db")
164164+ );
165165+ }
166166+167167+ #[test]
168168+ fn different_dids_produce_different_paths() {
169169+ let base = PathBuf::from("/data/dbs");
170170+ let a = did_to_db_path(&base, "did:plc:aaaa1111bbbb2222");
171171+ let b = did_to_db_path(&base, "did:plc:cccc3333dddd4444");
172172+ assert_ne!(a, b);
173173+ }
174174+175175+ #[test]
176176+ fn same_identifier_different_method_produces_different_paths() {
177177+ let base = PathBuf::from("/data/dbs");
178178+ let plc = did_to_db_path(&base, "did:plc:example.com");
179179+ let web = did_to_db_path(&base, "did:web:example.com");
180180+ assert_ne!(plc, web);
181181+ assert_eq!(plc.parent(), web.parent());
182182+ }
183183+}
+3
onis-common/src/lexicons/mod.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+pub mod record;
33+pub mod systems;
+35
onis-common/src/lexicons/record.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!A collection of known record types.
33+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
44+#[serde(tag = "$type")]
55+pub enum KnownRecord {
66+ #[serde(rename = "systems.kiri.dns")]
77+ LexiconsSystemsKiriDns(Box<crate::lexicons::systems::kiri::dns::Record>),
88+ #[serde(rename = "systems.kiri.zone")]
99+ LexiconsSystemsKiriZone(Box<crate::lexicons::systems::kiri::zone::Record>),
1010+}
1111+impl From<crate::lexicons::systems::kiri::dns::Record> for KnownRecord {
1212+ fn from(record: crate::lexicons::systems::kiri::dns::Record) -> Self {
1313+ KnownRecord::LexiconsSystemsKiriDns(Box::new(record))
1414+ }
1515+}
1616+impl From<crate::lexicons::systems::kiri::dns::RecordData> for KnownRecord {
1717+ fn from(record_data: crate::lexicons::systems::kiri::dns::RecordData) -> Self {
1818+ KnownRecord::LexiconsSystemsKiriDns(Box::new(record_data.into()))
1919+ }
2020+}
2121+impl From<crate::lexicons::systems::kiri::zone::Record> for KnownRecord {
2222+ fn from(record: crate::lexicons::systems::kiri::zone::Record) -> Self {
2323+ KnownRecord::LexiconsSystemsKiriZone(Box::new(record))
2424+ }
2525+}
2626+impl From<crate::lexicons::systems::kiri::zone::RecordData> for KnownRecord {
2727+ fn from(record_data: crate::lexicons::systems::kiri::zone::RecordData) -> Self {
2828+ KnownRecord::LexiconsSystemsKiriZone(Box::new(record_data.into()))
2929+ }
3030+}
3131+impl Into<atrium_api::types::Unknown> for KnownRecord {
3232+ fn into(self) -> atrium_api::types::Unknown {
3333+ atrium_api::types::TryIntoUnknown::try_into_unknown(&self).unwrap()
3434+ }
3535+}
+3
onis-common/src/lexicons/systems.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `systems` namespace.
33+pub mod kiri;
+16
onis-common/src/lexicons/systems/kiri.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `systems.kiri` namespace.
33+pub mod dns;
44+pub mod zone;
55+#[derive(Debug)]
66+pub struct Dns;
77+impl atrium_api::types::Collection for Dns {
88+ const NSID: &'static str = "systems.kiri.dns";
99+ type Record = dns::Record;
1010+}
1111+#[derive(Debug)]
1212+pub struct Zone;
1313+impl atrium_api::types::Collection for Zone {
1414+ const NSID: &'static str = "systems.kiri.zone";
1515+ type Record = zone::Record;
1616+}
+109
onis-common/src/lexicons/systems/kiri/dns.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `systems.kiri.dns` namespace.
33+use atrium_api::types::TryFromUnknown;
44+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
55+#[serde(rename_all = "camelCase")]
66+pub struct RecordData {
77+ ///Fully qualified domain name, lowercase, no trailing dot.
88+ pub domain: String,
99+ ///Attached note for informational reasons, not used by dns.
1010+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
1111+ pub note: core::option::Option<String>,
1212+ ///The DNS record data.
1313+ pub record: atrium_api::types::Union<RecordRecordRefs>,
1414+ ///Time to live in seconds. Optional — falls back to SOA minimum for the zone if omitted. Server enforces a 60s floor.
1515+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
1616+ pub ttl: core::option::Option<atrium_api::types::LimitedU32<2147483647u32>>,
1717+}
1818+pub type Record = atrium_api::types::Object<RecordData>;
1919+impl From<atrium_api::types::Unknown> for RecordData {
2020+ fn from(value: atrium_api::types::Unknown) -> Self {
2121+ Self::try_from_unknown(value).unwrap()
2222+ }
2323+}
2424+///A 32 bit Internet address.
2525+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
2626+#[serde(rename_all = "camelCase")]
2727+pub struct ARecordData {
2828+ ///IPv4 address in dotted-decimal notation.
2929+ pub address: String,
3030+}
3131+pub type ARecord = atrium_api::types::Object<ARecordData>;
3232+///An 128 bit Internet address.
3333+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
3434+#[serde(rename_all = "camelCase")]
3535+pub struct AaaaRecordData {
3636+ ///IPv6 address in standard notation.
3737+ pub address: String,
3838+}
3939+pub type AaaaRecord = atrium_api::types::Object<AaaaRecordData>;
4040+///A domain name which specifies the canonical or primary name for the owner.
4141+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
4242+#[serde(rename_all = "camelCase")]
4343+pub struct CnameRecordData {
4444+ ///The canonical domain name.
4545+ pub cname: String,
4646+}
4747+pub type CnameRecord = atrium_api::types::Object<CnameRecordData>;
4848+///A mail exchange record.
4949+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
5050+#[serde(rename_all = "camelCase")]
5151+pub struct MxRecordData {
5252+ ///The mail server hostname.
5353+ pub exchange: String,
5454+ ///Priority value. Lower is preferred.
5555+ pub preference: u16,
5656+}
5757+pub type MxRecord = atrium_api::types::Object<MxRecordData>;
5858+///Start of authority. Optional — system generates defaults if absent. Serial is also generated.
5959+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
6060+#[serde(rename_all = "camelCase")]
6161+pub struct SoaRecordData {
6262+ pub expire: usize,
6363+ ///Minimum TTL / negative cache TTL. Also used as the fallback TTL for records in this zone that don't specify one.
6464+ pub minimum: usize,
6565+ ///Primary nameserver.
6666+ pub mname: String,
6767+ pub refresh: usize,
6868+ pub retry: usize,
6969+ ///Responsible person email in DNS format (e.g. admin.example.com).
7070+ pub rname: String,
7171+}
7272+pub type SoaRecord = atrium_api::types::Object<SoaRecordData>;
7373+///A service locator record.
7474+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
7575+#[serde(rename_all = "camelCase")]
7676+pub struct SrvRecordData {
7777+ pub port: u16,
7878+ pub priority: u16,
7979+ ///The target hostname.
8080+ pub target: String,
8181+ pub weight: u16,
8282+}
8383+pub type SrvRecord = atrium_api::types::Object<SrvRecordData>;
8484+///A text record. TXT-DATA is one or more character strings.
8585+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
8686+#[serde(rename_all = "camelCase")]
8787+pub struct TxtRecordData {
8888+ ///One or more character strings. Each string has a 255-byte max on the wire.
8989+ pub values: Vec<String>,
9090+}
9191+pub type TxtRecord = atrium_api::types::Object<TxtRecordData>;
9292+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
9393+#[serde(tag = "$type")]
9494+pub enum RecordRecordRefs {
9595+ #[serde(rename = "systems.kiri.dns#aRecord")]
9696+ ARecord(Box<ARecord>),
9797+ #[serde(rename = "systems.kiri.dns#aaaaRecord")]
9898+ AaaaRecord(Box<AaaaRecord>),
9999+ #[serde(rename = "systems.kiri.dns#cnameRecord")]
100100+ CnameRecord(Box<CnameRecord>),
101101+ #[serde(rename = "systems.kiri.dns#mxRecord")]
102102+ MxRecord(Box<MxRecord>),
103103+ #[serde(rename = "systems.kiri.dns#txtRecord")]
104104+ TxtRecord(Box<TxtRecord>),
105105+ #[serde(rename = "systems.kiri.dns#srvRecord")]
106106+ SrvRecord(Box<SrvRecord>),
107107+ #[serde(rename = "systems.kiri.dns#soaRecord")]
108108+ SoaRecord(Box<SoaRecord>),
109109+}
+18
onis-common/src/lexicons/systems/kiri/zone.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `systems.kiri.zone` namespace.
33+use atrium_api::types::TryFromUnknown;
44+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
55+#[serde(rename_all = "camelCase")]
66+pub struct RecordData {
77+ ///The zone apex domain. Lowercase, no trailing dot.
88+ pub domain: String,
99+ ///Attached note for informational reasons, not used by dns.
1010+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
1111+ pub note: core::option::Option<String>,
1212+}
1313+pub type Record = atrium_api::types::Object<RecordData>;
1414+impl From<atrium_api::types::Unknown> for RecordData {
1515+ fn from(value: atrium_api::types::Unknown) -> Self {
1616+ Self::try_from_unknown(value).unwrap()
1717+ }
1818+}
+4
onis-common/src/lib.rs
···11+pub mod config;
22+pub mod db;
33+pub mod lexicons;
44+pub mod metrics;
+30
onis-common/src/metrics.rs
···11+use axum::extract::State;
22+use axum::http::StatusCode;
33+use axum::response::IntoResponse;
44+use axum::routing::get;
55+use axum::Router;
66+77+pub use metrics_exporter_prometheus::PrometheusHandle;
88+99+use metrics_exporter_prometheus::PrometheusBuilder;
1010+1111+/// Initialize the global metrics recorder and return a handle for rendering.
1212+pub fn init() -> Result<PrometheusHandle, metrics_exporter_prometheus::BuildError> {
1313+ PrometheusBuilder::new().install_recorder()
1414+}
1515+1616+/// Axum handler that renders all registered metrics in Prometheus exposition format.
1717+pub async fn metrics_handler(State(handle): State<PrometheusHandle>) -> impl IntoResponse {
1818+ (
1919+ StatusCode::OK,
2020+ [("content-type", "text/plain; version=0.0.4; charset=utf-8")],
2121+ handle.render(),
2222+ )
2323+}
2424+2525+/// Returns a minimal axum router with just `GET /metrics`.
2626+pub fn router(handle: PrometheusHandle) -> Router {
2727+ Router::new()
2828+ .route("/metrics", get(metrics_handler))
2929+ .with_state(handle)
3030+}