An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: address PR review feedback for delete_handle

- Check rows_affected() on DELETE; return 404 if zero (prevents silent
no-op when handle is concurrently removed after DNS deletion)
- Add tracing::warn\! when dns_provider is None so operators know DNS
was not cleaned up
- Add dns_record_name and did to Step 4 error log; add did + clarified
message to Step 5 error log
- Move AlwaysOkDns/AlwaysErrDns/state_with_ok_dns/state_with_err_dns
to routes/test_utils.rs; update create_handle.rs and delete_handle.rs
to import from there
- Fix alphabetical import ordering in app.rs and routes/mod.rs
- Add delete_handle.rs row to relay/CLAUDE.md route table
- Strengthen delete_record doc; update dns.rs module comment

authored by malpercio.dev and committed by

Tangled c11087a6 fe5ec7f2

+98 -126
+1
crates/relay/CLAUDE.md
··· 75 75 | `get_did.rs` | `GET /v1/dids/:did` | 76 76 | `create_account.rs` | `POST /v1/accounts` | 77 77 | `create_handle.rs` | `POST /v1/handles` | 78 + | `delete_handle.rs` | `DELETE /v1/handles/:handle` | 78 79 | `create_mobile_account.rs` | `POST /v1/accounts/mobile` | 79 80 | `create_signing_key.rs` | `POST /v1/signing-keys` | 80 81 | `register_device.rs` | `POST /v1/devices` |
+1 -1
crates/relay/src/app.rs
··· 22 22 use crate::routes::create_did::create_did_handler; 23 23 use crate::routes::create_handle::create_handle_handler; 24 24 use crate::routes::create_mobile_account::create_mobile_account; 25 - use crate::routes::delete_handle::delete_handle_handler; 26 25 use crate::routes::create_session::create_session; 27 26 use crate::routes::create_signing_key::create_signing_key; 27 + use crate::routes::delete_handle::delete_handle_handler; 28 28 use crate::routes::delete_session::delete_session; 29 29 use crate::routes::describe_server::describe_server; 30 30 use crate::routes::get_device_relay::get_device_relay;
+7 -2
crates/relay/src/dns.rs
··· 1 1 // DNS abstractions for handle management. 2 2 // 3 - // DnsProvider — creates DNS records when handles are registered (POST /v1/handles). 3 + // DnsProvider — manages DNS records for handles: 4 + // - create_record: called when handles are registered (POST /v1/handles). 5 + // - delete_record: called when handles are removed (DELETE /v1/handles/:handle). 4 6 // For v0.1, AppState carries `dns_provider: None`; operators manage DNS manually. 5 7 // Real provider implementations (Cloudflare, Route53) are wired in when configured. 6 8 // ··· 103 105 /// Delete the DNS record for `name` (a subdomain label, e.g. `"alice"`). 104 106 /// 105 107 /// Called when a handle is deleted. The provider is responsible for locating 106 - /// the record by name within its configured zone. 108 + /// and removing the record within its configured zone. 109 + /// 110 + /// Callers are responsible for extracting the label portion from the full handle 111 + /// before invoking this method (e.g. pass `"alice"`, not `"alice.example.com"`). 107 112 fn delete_record<'a>( 108 113 &'a self, 109 114 name: &'a str,
+1 -58
crates/relay/src/routes/create_handle.rs
··· 163 163 mod tests { 164 164 use super::*; 165 165 use crate::app::test_state; 166 + use crate::routes::test_utils::{state_with_err_dns, state_with_ok_dns}; 166 167 use crate::routes::token::generate_token; 167 168 use axum::{ 168 169 body::Body, 169 170 http::{Request, StatusCode}, 170 171 }; 171 - use std::future::Future; 172 - use std::pin::Pin; 173 - use std::sync::Arc; 174 172 use tower::ServiceExt; 175 173 use uuid::Uuid; 176 174 ··· 244 242 let domains = vec!["example.com".to_string()]; 245 243 let name = "a".repeat(63); 246 244 assert!(validate_handle(&format!("{name}.example.com"), &domains).is_ok()); 247 - } 248 - 249 - // ── DNS provider test doubles ────────────────────────────────────────────── 250 - 251 - struct AlwaysOkDns; 252 - struct AlwaysErrDns; 253 - 254 - impl crate::dns::DnsProvider for AlwaysOkDns { 255 - fn create_record<'a>( 256 - &'a self, 257 - _name: &'a str, 258 - _target: &'a str, 259 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 260 - Box::pin(async { Ok(()) }) 261 - } 262 - 263 - fn delete_record<'a>( 264 - &'a self, 265 - _name: &'a str, 266 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 267 - Box::pin(async { Ok(()) }) 268 - } 269 - } 270 - 271 - impl crate::dns::DnsProvider for AlwaysErrDns { 272 - fn create_record<'a>( 273 - &'a self, 274 - _name: &'a str, 275 - _target: &'a str, 276 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 277 - Box::pin(async { Err(crate::dns::DnsError("simulated provider error".to_string())) }) 278 - } 279 - 280 - fn delete_record<'a>( 281 - &'a self, 282 - _name: &'a str, 283 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 284 - Box::pin(async { Err(crate::dns::DnsError("simulated provider error".to_string())) }) 285 - } 286 - } 287 - 288 - async fn state_with_ok_dns() -> crate::app::AppState { 289 - let base = test_state().await; 290 - crate::app::AppState { 291 - dns_provider: Some(Arc::new(AlwaysOkDns)), 292 - ..base 293 - } 294 - } 295 - 296 - async fn state_with_err_dns() -> crate::app::AppState { 297 - let base = test_state().await; 298 - crate::app::AppState { 299 - dns_provider: Some(Arc::new(AlwaysErrDns)), 300 - ..base 301 - } 302 245 } 303 246 304 247 // ── Integration test helpers ───────────────────────────────────────────────
+23 -64
crates/relay/src/routes/delete_handle.rs
··· 14 14 // (DNS deletion precedes DB deletion: a DB row without a DNS record is operator-fixable; 15 15 // a DNS record without a DB row is an invisible orphan that can corrupt future 16 16 // registrations for the same subdomain) 17 - // 5. DELETE FROM handles WHERE handle = ? 17 + // If state.dns_provider is None: emit tracing::warn! (no DNS cleanup performed) 18 + // 5. DELETE FROM handles WHERE handle = ?; check rows_affected() → 404 HANDLE_NOT_FOUND if zero 18 19 // 6. Return 204 No Content 19 20 // 20 21 // Outputs (success): 204 No Content ··· 63 64 // DNS deletion precedes DB deletion: a DB row without a DNS record is operator-fixable 64 65 // (admin can retry), whereas a DNS record without a DB row is an invisible orphan that 65 66 // could corrupt a future handle registration for the same subdomain. 67 + let name = handle.split_once('.').map(|(n, _)| n).unwrap_or(&handle); 66 68 if let Some(provider) = &state.dns_provider { 67 - let name = handle.split_once('.').map(|(n, _)| n).unwrap_or(&handle); 68 69 provider.delete_record(name).await.map_err(|e| { 69 70 tracing::error!( 70 71 error = %e, 71 72 handle = %handle, 73 + dns_record_name = %name, 72 74 did = %session.did, 73 75 "DNS record deletion failed" 74 76 ); 75 77 ApiError::new(ErrorCode::DnsError, "failed to delete DNS record") 76 78 })?; 79 + } else { 80 + tracing::warn!( 81 + handle = %handle, 82 + did = %session.did, 83 + "no DNS provider configured; DNS record for handle was not cleaned up" 84 + ); 77 85 } 78 86 79 - // Step 5: Delete the handle row. 80 - sqlx::query("DELETE FROM handles WHERE handle = ?") 87 + // Step 5: Delete the handle row; 404 if the row was concurrently removed. 88 + let result = sqlx::query("DELETE FROM handles WHERE handle = ?") 81 89 .bind(&handle) 82 90 .execute(&state.db) 83 91 .await 84 92 .map_err(|e| { 85 - tracing::error!(error = %e, handle = %handle, "failed to delete handle row"); 93 + tracing::error!( 94 + error = %e, 95 + handle = %handle, 96 + did = %session.did, 97 + "failed to delete handle row after DNS deletion; manual DB cleanup required" 98 + ); 86 99 ApiError::new(ErrorCode::InternalError, "failed to delete handle") 87 100 })?; 101 + 102 + if result.rows_affected() == 0 { 103 + return Err(ApiError::new(ErrorCode::HandleNotFound, "handle not found")); 104 + } 88 105 89 106 // Step 6: Return 204 No Content. 90 107 Ok(StatusCode::NO_CONTENT) ··· 95 112 #[cfg(test)] 96 113 mod tests { 97 114 use crate::app::test_state; 98 - use crate::routes::test_utils::seed_handle; 115 + use crate::routes::test_utils::{seed_handle, state_with_err_dns, state_with_ok_dns}; 99 116 use crate::routes::token::generate_token; 100 117 use axum::{ 101 118 body::Body, 102 119 http::{Request, StatusCode}, 103 120 }; 104 - use std::future::Future; 105 - use std::pin::Pin; 106 - use std::sync::Arc; 107 121 use tower::ServiceExt; 108 122 use uuid::Uuid; 109 - 110 - // ── DNS provider test doubles ────────────────────────────────────────────── 111 - 112 - struct AlwaysOkDns; 113 - struct AlwaysErrDns; 114 - 115 - impl crate::dns::DnsProvider for AlwaysOkDns { 116 - fn create_record<'a>( 117 - &'a self, 118 - _name: &'a str, 119 - _target: &'a str, 120 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 121 - Box::pin(async { Ok(()) }) 122 - } 123 - 124 - fn delete_record<'a>( 125 - &'a self, 126 - _name: &'a str, 127 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 128 - Box::pin(async { Ok(()) }) 129 - } 130 - } 131 - 132 - impl crate::dns::DnsProvider for AlwaysErrDns { 133 - fn create_record<'a>( 134 - &'a self, 135 - _name: &'a str, 136 - _target: &'a str, 137 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 138 - Box::pin(async { Err(crate::dns::DnsError("simulated provider error".to_string())) }) 139 - } 140 - 141 - fn delete_record<'a>( 142 - &'a self, 143 - _name: &'a str, 144 - ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 145 - Box::pin(async { Err(crate::dns::DnsError("simulated provider error".to_string())) }) 146 - } 147 - } 148 - 149 - async fn state_with_ok_dns() -> crate::app::AppState { 150 - let base = test_state().await; 151 - crate::app::AppState { 152 - dns_provider: Some(Arc::new(AlwaysOkDns)), 153 - ..base 154 - } 155 - } 156 - 157 - async fn state_with_err_dns() -> crate::app::AppState { 158 - let base = test_state().await; 159 - crate::app::AppState { 160 - dns_provider: Some(Arc::new(AlwaysErrDns)), 161 - ..base 162 - } 163 - } 164 123 165 124 // ── Test session helpers ─────────────────────────────────────────────────── 166 125
+1 -1
crates/relay/src/routes/mod.rs
··· 5 5 pub mod create_did; 6 6 pub mod create_handle; 7 7 pub mod create_mobile_account; 8 - pub mod delete_handle; 9 8 pub mod create_session; 10 9 pub mod create_signing_key; 10 + pub mod delete_handle; 11 11 pub mod delete_session; 12 12 pub mod describe_server; 13 13 pub mod get_device_relay;
+64
crates/relay/src/routes/test_utils.rs
··· 1 + use std::future::Future; 2 + use std::pin::Pin; 1 3 use std::sync::Arc; 2 4 3 5 use argon2::{ ··· 6 8 }; 7 9 8 10 use crate::app::{test_state, AppState}; 11 + 12 + // ── DNS provider test doubles ────────────────────────────────────────────── 13 + 14 + /// DNS provider that succeeds on every `create_record` and `delete_record` call. 15 + pub struct AlwaysOkDns; 16 + 17 + /// DNS provider that fails on every `create_record` and `delete_record` call. 18 + pub struct AlwaysErrDns; 19 + 20 + impl crate::dns::DnsProvider for AlwaysOkDns { 21 + fn create_record<'a>( 22 + &'a self, 23 + _name: &'a str, 24 + _target: &'a str, 25 + ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 26 + Box::pin(async { Ok(()) }) 27 + } 28 + 29 + fn delete_record<'a>( 30 + &'a self, 31 + _name: &'a str, 32 + ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 33 + Box::pin(async { Ok(()) }) 34 + } 35 + } 36 + 37 + impl crate::dns::DnsProvider for AlwaysErrDns { 38 + fn create_record<'a>( 39 + &'a self, 40 + _name: &'a str, 41 + _target: &'a str, 42 + ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 43 + Box::pin(async { Err(crate::dns::DnsError("simulated provider error".to_string())) }) 44 + } 45 + 46 + fn delete_record<'a>( 47 + &'a self, 48 + _name: &'a str, 49 + ) -> Pin<Box<dyn Future<Output = Result<(), crate::dns::DnsError>> + Send + 'a>> { 50 + Box::pin(async { Err(crate::dns::DnsError("simulated provider error".to_string())) }) 51 + } 52 + } 53 + 54 + /// `test_state()` with an `AlwaysOkDns` provider wired in. 55 + pub async fn state_with_ok_dns() -> AppState { 56 + let base = test_state().await; 57 + AppState { 58 + dns_provider: Some(Arc::new(AlwaysOkDns)), 59 + ..base 60 + } 61 + } 62 + 63 + /// `test_state()` with an `AlwaysErrDns` provider wired in. 64 + pub async fn state_with_err_dns() -> AppState { 65 + let base = test_state().await; 66 + AppState { 67 + dns_provider: Some(Arc::new(AlwaysErrDns)), 68 + ..base 69 + } 70 + } 71 + 72 + // ── Admin state helper ──────────────────────────────────────────────────────── 9 73 10 74 /// Minimal test state with admin_token set to `"test-admin-token"`. 11 75 ///