Server: - POST /v1/alias/register — claim an alias (one per fingerprint) - GET /v1/alias/resolve/:name — alias → fingerprint - GET /v1/alias/whois/:fingerprint — fingerprint → alias (reverse) - GET /v1/alias/list — list all aliases - Bidirectional mapping in sled (a:name→fp, fp:fp→name) - One alias per person, re-registering replaces old alias Web client: - /alias <name> — register your alias - /aliases — list all registered aliases - /info — now shows alias alongside fingerprint - Peer input accepts @alias (resolved before sending) - Received messages show @alias instead of fingerprint - DM: paste @alias or fingerprint in peer input CLI TUI: - /alias <name> — register alias - /aliases — list all aliases - /peer @alias — resolves alias to fingerprint - Alias resolution displayed in system messages Addressing model: - @manwe (local) → server resolves → fingerprint - @manwe.b1.example.com (federated) → DNS resolve (Phase 3) - Raw fingerprint → always works, no resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
128 lines
4.1 KiB
Rust
128 lines
4.1 KiB
Rust
use axum::{
|
|
extract::{Path, State},
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use serde::Deserialize;
|
|
|
|
use crate::errors::AppResult;
|
|
use crate::state::AppState;
|
|
|
|
pub fn routes() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/alias/register", post(register_alias))
|
|
.route("/alias/resolve/:name", get(resolve_alias))
|
|
.route("/alias/list", get(list_aliases))
|
|
.route("/alias/whois/:fingerprint", get(reverse_lookup))
|
|
}
|
|
|
|
fn normalize_fp(fp: &str) -> String {
|
|
fp.chars()
|
|
.filter(|c| c.is_ascii_hexdigit())
|
|
.collect::<String>()
|
|
.to_lowercase()
|
|
}
|
|
|
|
fn normalize_alias(name: &str) -> String {
|
|
name.trim()
|
|
.to_lowercase()
|
|
.chars()
|
|
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
|
|
.collect()
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct RegisterRequest {
|
|
alias: String,
|
|
fingerprint: String,
|
|
}
|
|
|
|
/// Register an alias. One alias per fingerprint, one fingerprint per alias.
|
|
async fn register_alias(
|
|
State(state): State<AppState>,
|
|
Json(req): Json<RegisterRequest>,
|
|
) -> AppResult<Json<serde_json::Value>> {
|
|
let alias = normalize_alias(&req.alias);
|
|
let fp = normalize_fp(&req.fingerprint);
|
|
|
|
if alias.is_empty() || alias.len() > 32 {
|
|
return Ok(Json(serde_json::json!({ "error": "alias must be 1-32 alphanumeric chars" })));
|
|
}
|
|
|
|
// Check if alias is taken by someone else
|
|
if let Some(existing) = state.db.aliases.get(format!("a:{}", alias).as_bytes())? {
|
|
let existing_fp = String::from_utf8_lossy(&existing).to_string();
|
|
if existing_fp != fp {
|
|
return Ok(Json(serde_json::json!({ "error": "alias already taken" })));
|
|
}
|
|
// Same person re-registering — ok
|
|
}
|
|
|
|
// Remove old alias for this fingerprint (one alias per person)
|
|
if let Some(old_alias) = state.db.aliases.get(format!("fp:{}", fp).as_bytes())? {
|
|
let old = String::from_utf8_lossy(&old_alias).to_string();
|
|
state.db.aliases.remove(format!("a:{}", old).as_bytes())?;
|
|
tracing::info!("Removed old alias '{}' for {}", old, fp);
|
|
}
|
|
|
|
// Store both directions: alias→fp and fp→alias
|
|
state.db.aliases.insert(format!("a:{}", alias).as_bytes(), fp.as_bytes())?;
|
|
state.db.aliases.insert(format!("fp:{}", fp).as_bytes(), alias.as_bytes())?;
|
|
state.db.aliases.flush()?;
|
|
|
|
tracing::info!("Alias '{}' registered for {}", alias, fp);
|
|
Ok(Json(serde_json::json!({ "ok": true, "alias": alias, "fingerprint": fp })))
|
|
}
|
|
|
|
/// Resolve an alias to a fingerprint.
|
|
async fn resolve_alias(
|
|
State(state): State<AppState>,
|
|
Path(name): Path<String>,
|
|
) -> AppResult<Json<serde_json::Value>> {
|
|
let alias = normalize_alias(&name);
|
|
|
|
match state.db.aliases.get(format!("a:{}", alias).as_bytes())? {
|
|
Some(data) => {
|
|
let fp = String::from_utf8_lossy(&data).to_string();
|
|
Ok(Json(serde_json::json!({ "alias": alias, "fingerprint": fp })))
|
|
}
|
|
None => Ok(Json(serde_json::json!({ "error": "alias not found" }))),
|
|
}
|
|
}
|
|
|
|
/// Reverse lookup: fingerprint → alias.
|
|
async fn reverse_lookup(
|
|
State(state): State<AppState>,
|
|
Path(fingerprint): Path<String>,
|
|
) -> AppResult<Json<serde_json::Value>> {
|
|
let fp = normalize_fp(&fingerprint);
|
|
|
|
match state.db.aliases.get(format!("fp:{}", fp).as_bytes())? {
|
|
Some(data) => {
|
|
let alias = String::from_utf8_lossy(&data).to_string();
|
|
Ok(Json(serde_json::json!({ "fingerprint": fp, "alias": alias })))
|
|
}
|
|
None => Ok(Json(serde_json::json!({ "fingerprint": fp, "alias": null }))),
|
|
}
|
|
}
|
|
|
|
/// List all aliases.
|
|
async fn list_aliases(
|
|
State(state): State<AppState>,
|
|
) -> AppResult<Json<serde_json::Value>> {
|
|
let aliases: Vec<serde_json::Value> = state
|
|
.db
|
|
.aliases
|
|
.scan_prefix(b"a:")
|
|
.filter_map(|item| {
|
|
item.ok().map(|(k, v)| {
|
|
let alias = String::from_utf8_lossy(&k[2..]).to_string();
|
|
let fp = String::from_utf8_lossy(&v).to_string();
|
|
serde_json::json!({ "alias": alias, "fingerprint": fp })
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(serde_json::json!({ "aliases": aliases, "count": aliases.len() })))
|
|
}
|