Take the link from wg-app-link instead of keeping a second copy

The five modules underneath this backend that were never about AI
sessions -- the pinned CA and leaf, QR enrollment and the bearer token,
wg0 binding and the certificate's SANs, owner-only files, and the RON
house rules -- were written twice, once here and once in dev-updater,
and had drifted. They now come from the submodule, as a path dependency
so both projects stay locked to one commit.

What stayed is what makes this project itself: the routes, the drivers,
the config schema, and the auth middleware, which is generic over this
server's state. Sharing a transport is worth doing; sharing an API would
mean inventing a vocabulary neither project wants.

Four dependencies go with the code -- rcgen, qrcode, subtle and if-addrs
are no longer named here at all -- and the three that remain are now
described by what still uses them rather than by what used to.

Verified by running it, not only by building: a fresh server generates
its CA, prints an `aiapp://enroll` QR with the scheme now passed as a
parameter, covers 127.0.0.1, 10.0.2.2 and wg0's 10.66.0.1 in the leaf,
answers an enrolled token and returns 401 without one, and writes
config.ron in the house rules with every file owner-only. 36 tests pass,
clippy is silent, rustfmt is clean.
This commit is contained in:
iris committed 2026-08-28 17:14:33 -04:00
1 parent a83dbcff6a
commit aa05ff9336
13 files changed
+76 -526

No files matched your search

+4 -60
View File
@@ -7,9 +7,10 @@
//! funnels through `SessionManager` (the registry pattern), so in-memory
//! and on-disk state can't come apart.
//!
//! The file is RON, in the shape the [`mod@format`] module describes -- the
//! The file is RON, in the shape [`wg_app_link::format`] describes -- the
//! same format, and the same two house rules, as the sibling dev-updater
//! project's config, because both are written and read by hand.
//! project's config, because both are written and read by hand, and both
//! now read and write them through the one module.
//!
//! Transcripts do NOT live here -- each session's events are an append-only
//! JSONL file in its own directory (see `session::transcript`); this file
@@ -21,64 +22,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::private;
/// Reading and writing the RON this file is in.
///
/// Two things are house rules rather than plain RON, and they are here
/// together because they are inverses of each other -- change one and the
/// other stops round-tripping.
///
/// **No outer parentheses.** A file *is* the body of the struct, so nothing
/// in it is indented for the sake of a wrapper. RON has no implicit
/// top-level struct (`de/mod.rs` requires the `(`), so [`format::parse`]
/// adds it and [`format::render`] takes it back off. The opening paren is
/// not followed by a newline, so a parse error's line number still points
/// at the real line.
///
/// **`Some` is implicit.** Enabled on the deserializer rather than by a
/// `#![enable(implicit_some)]` header the file would have to carry, and
/// matched on the writing side by `skip_serializing_if` on every optional
/// field so nothing writes back a `Some(...)` a person didn't type.
pub(crate) mod format {
use serde::{Serialize, de::DeserializeOwned};
fn options() -> ron::Options {
ron::Options::default().with_default_extension(ron::extensions::Extensions::IMPLICIT_SOME)
}
pub fn parse<T: DeserializeOwned>(text: &str) -> Result<T, ron::error::SpannedError> {
options().from_str(&format!("({text})"))
}
pub fn render<T: Serialize>(value: &T) -> Result<String, ron::Error> {
let pretty = ron::ser::PrettyConfig::new();
let text = options().to_string_pretty(value, pretty)?;
Ok(unwrap_outer(&text))
}
/// Strips the outer `(`/`)` the writer always emits and removes the
/// indent level they cost. Deliberately narrow: it accepts only the
/// exact shape `PrettyConfig` produces, and leaves anything else alone
/// rather than guessing -- a file with stray parentheses is better than
/// one silently mangled. `parse` round-trips either way, since a
/// wrapped body parses the same as an unwrapped one re-wrapped.
fn unwrap_outer(text: &str) -> String {
let Some(body) = text
.strip_prefix("(\n")
.and_then(|rest| rest.strip_suffix("\n)"))
else {
return text.to_string();
};
let mut out: String = body
.lines()
.map(|line| line.strip_prefix(" ").unwrap_or(line))
.collect::<Vec<_>>()
.join("\n");
out.push('\n');
out
}
}
use wg_app_link::{format, private};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]