feat(#2302): fold is_plain_ident into PlainIdent newtype

This commit is contained in:
damocles 2026-07-19 17:56:55 +02:00 committed by mara
commit d4e91bfeeb
3 changed files with 112 additions and 86 deletions

View file

@ -23,7 +23,7 @@ use axum::extract::{Form, Query};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use super::error_response;
use super::{error_response, idents::PlainIdent};
use crate::coordinator::Coordinator;
#[derive(Deserialize)]
@ -176,17 +176,6 @@ struct MatrixLoginResult {
user_id: String,
}
/// Plain-identifier check matching hive-priv's `validate_name_chars`
/// exactly (lowercase ascii + digits + hyphens) — the root-side guard
/// re-applies the same rule before building the token path. Keeping the
/// dashboard check identical means a name that passes here can't then be
/// rejected at the priv boundary with a confusing "write token failed".
fn is_plain_ident(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
/// Provision (or refresh) the token for an agent's extra matrix account.
/// password mode → `m.login.password`; token mode → validate via `whoami`.
/// On success writes the token to `matrix-token-<account>` via hive-priv and
@ -196,15 +185,15 @@ pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) ->
let agent = f.agent.trim();
let account = f.account.trim();
let homeserver = f.homeserver.trim().trim_end_matches('/');
if !is_plain_ident(agent) {
let Ok(agent) = PlainIdent::parse(agent) else {
return error_response(&format!("matrix-account-login: invalid agent {agent:?}"));
}
if !is_plain_ident(account) {
};
let Ok(account) = PlainIdent::parse(account) else {
return error_response(&format!(
"matrix-account-login: invalid account {account:?}"
));
}
if account == "main" {
};
if account.as_str() == "main" {
return error_response(
"matrix-account-login: 'main' is the hive-internal account; it is \
provisioned via the normal flow, not this form",
@ -244,15 +233,19 @@ pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) ->
}
};
if let Err(e) =
crate::priv_client::write_agent_matrix_token(agent, &token, Some(account), Some(homeserver))
.await
if let Err(e) = crate::priv_client::write_agent_matrix_token(
agent.as_str(),
&token,
Some(account.as_str()),
Some(homeserver),
)
.await
{
return error_response(&format!("matrix-account-login: write token failed: {e:#}"));
}
// Best-effort kick so the daemon picks up the new account without a full
// container restart; not fatal if the container isn't running.
if let Err(e) = crate::priv_client::restart_matrix_daemon(agent).await {
if let Err(e) = crate::priv_client::restart_matrix_daemon(agent.as_str()).await {
tracing::warn!(
%agent, %account, error = ?e,
"matrix-account-login: daemon restart failed (token written; loads on next start)"
@ -288,13 +281,13 @@ struct GithubAccountResult {
pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Response {
let agent = f.agent.trim();
let token = f.token.trim();
if !is_plain_ident(agent) {
let Ok(agent) = PlainIdent::parse(agent) else {
return error_response(&format!("github-account: invalid agent {agent:?}"));
}
};
if token.is_empty() {
return error_response("github-account: token is required");
}
if let Err(e) = crate::priv_client::write_agent_github_token(agent, token).await {
if let Err(e) = crate::priv_client::write_agent_github_token(agent.as_str(), token).await {
return error_response(&format!("github-account: write token failed: {e:#}"));
}
tracing::info!(%agent, "github-account: provisioned github PAT");
@ -320,10 +313,10 @@ struct GithubAccountStatus {
/// Never returns the token itself.
pub(super) async fn get_github_account(Query(q): Query<GithubAccountQuery>) -> Response {
let agent = q.agent.trim();
if !is_plain_ident(agent) {
let Ok(agent) = PlainIdent::parse(agent) else {
return error_response(&format!("github-account: invalid agent {agent:?}"));
}
let present = Coordinator::agent_notes_dir(agent)
};
let present = Coordinator::agent_notes_dir(agent.as_str())
.join("github-token")
.exists();
axum::Json(GithubAccountStatus { present }).into_response()
@ -402,7 +395,7 @@ async fn matrix_whoami(homeserver: &str, token: &str) -> Result<String, String>
#[cfg(test)]
mod tests {
use super::{account_name_from_filename, is_plain_ident, read_accounts_snapshot};
use super::{account_name_from_filename, read_accounts_snapshot};
fn unique_dir(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!(
@ -488,20 +481,4 @@ mod tests {
assert_eq!(account_name_from_filename("notes.md"), None);
assert_eq!(account_name_from_filename("matrix-avatar-icon-hash"), None);
}
#[test]
fn is_plain_ident_matches_validate_name_chars() {
// Accepts exactly what hive-priv's validate_name_chars does:
// lowercase ascii + digits + hyphens.
assert!(is_plain_ident("catgirl"));
assert!(is_plain_ident("acct-1"));
assert!(!is_plain_ident(""));
// Rejected: uppercase + underscore (would pass a looser check
// then fail at the priv boundary), and path chars.
assert!(!is_plain_ident("MyAccount"));
assert!(!is_plain_ident("my_account"));
assert!(!is_plain_ident("../escape"));
assert!(!is_plain_ident("a/b"));
assert!(!is_plain_ident("a.b"));
}
}