Our Personal Data Server from scratch!
0

Configure Feed

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

db: Did type for repos and handlers

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jul 12, 2026, 8:03 AM +0200) commit eb1a89dc parent 6ca6c456 change-id ppszswoy
+421 -444
+1 -1
crates/tranquil-api/src/admin/account/info.rs
··· 200 200 return Err(ApiError::InvalidRequest("dids is required".into())); 201 201 } 202 202 203 - let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 203 + let dids: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 204 204 let accounts = state 205 205 .repos 206 206 .infra
+4 -4
crates/tranquil-api/src/admin/invite.rs
··· 15 15 #[serde(rename_all = "camelCase")] 16 16 pub struct DisableInviteCodesInput { 17 17 pub codes: Option<Vec<String>>, 18 - pub accounts: Option<Vec<String>>, 18 + pub accounts: Option<Vec<Did>>, 19 19 } 20 20 21 21 pub async fn disable_invite_codes( ··· 97 97 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 98 98 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); 99 99 100 - let creator_dids: std::collections::HashMap<uuid::Uuid, tranquil_types::Did> = state 100 + let creator_dids: std::collections::HashMap<uuid::Uuid, Did> = state 101 101 .repos 102 102 .infra 103 103 .get_user_dids_by_ids(&user_ids) ··· 167 167 if account.is_empty() { 168 168 return Err(ApiError::InvalidRequest("account is required".into())); 169 169 } 170 - let account_did: tranquil_types::Did = account 170 + let account_did: Did = account 171 171 .parse() 172 172 .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 173 173 ··· 200 200 if account.is_empty() { 201 201 return Err(ApiError::InvalidRequest("account is required".into())); 202 202 } 203 - let account_did: tranquil_types::Did = account 203 + let account_did: Did = account 204 204 .parse() 205 205 .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 206 206
+2 -2
crates/tranquil-api/src/common.rs
··· 195 195 } 196 196 } 197 197 198 - pub fn create_self_hosted_did_web(handle: &str) -> Result<String, ApiError> { 198 + pub fn create_self_hosted_did_web(handle: &str) -> Result<Did, ApiError> { 199 199 if !tranquil_pds::util::is_self_hosted_did_web_enabled() { 200 200 return Err(ApiError::SelfHostedDidWebDisabled); 201 201 } 202 202 let encoded_handle = handle.replace(':', "%3A"); 203 - Ok(format!("did:web:{}", encoded_handle)) 203 + Ok(Did::from(format!("did:web:{}", encoded_handle))) 204 204 } 205 205 206 206 pub enum CredentialMatch {
+2 -2
crates/tranquil-api/src/delegation.rs
··· 38 38 async move { 39 39 if c.handle.is_none() { 40 40 c.handle = did_resolver 41 - .fetch_did_document(c.did.as_str()) 41 + .fetch_did_document(&c.did) 42 42 .await 43 43 .ok() 44 44 .and_then(|doc| tranquil_types::did_doc::extract_handle(&doc)); ··· 77 77 } 78 78 match state 79 79 .cross_pds_oauth 80 - .check_remote_is_delegated(pds_url, input.controller_did.as_str()) 80 + .check_remote_is_delegated(pds_url, &input.controller_did) 81 81 .await 82 82 { 83 83 Some(true) => {
+8 -8
crates/tranquil-api/src/identity/account.rs
··· 25 25 pub invite_code: Option<String>, 26 26 pub did: Option<String>, 27 27 pub did_type: Option<String>, 28 - pub signing_key: Option<String>, 28 + pub signing_key: Option<Did>, 29 29 pub verification_channel: Option<tranquil_db_traits::CommsChannel>, 30 30 pub discord_username: Option<String>, 31 31 pub telegram_username: Option<String>, ··· 47 47 48 48 async fn try_reactivate_migration( 49 49 state: &AppState, 50 - did: &str, 50 + did: &Did, 51 51 handle: &Handle, 52 52 email: &Option<String>, 53 53 verification_channel: tranquil_db_traits::CommsChannel, ··· 112 112 } 113 113 }; 114 114 let session_data = tranquil_db_traits::SessionTokenCreate { 115 - did: did_typed.clone(), 115 + did: did.clone(), 116 116 access_jti: access_meta.jti.clone(), 117 117 refresh_jti: refresh_meta.jti.clone(), 118 118 access_expires_at: access_meta.expires_at, ··· 132 132 super::provision::enqueue_migration_verification( 133 133 state, 134 134 reactivated.user_id, 135 - &did_typed, 135 + did, 136 136 verification_channel, 137 137 recipient, 138 138 ) ··· 309 309 let signing_key = key_result.signing_key; 310 310 let reserved_key_id = key_result.reserved_key_id; 311 311 let did_type = input.did_type.as_deref().unwrap_or("plc"); 312 - let did = match did_type { 312 + let did: Did = match did_type { 313 313 "web" => { 314 314 let self_hosted_did = match common::create_self_hosted_did_web(&handle) { 315 315 Ok(d) => d, ··· 456 456 let create_input = tranquil_db_traits::CreatePasswordAccountInput { 457 457 handle: handle.clone(), 458 458 email: email.clone(), 459 - did: did_for_commit.clone(), 459 + did: did.clone(), 460 460 password_hash, 461 461 preferred_comms_channel, 462 462 discord_username: comms.discord, ··· 508 508 super::provision::enqueue_signup_verification( 509 509 &state, 510 510 user_id, 511 - &did_for_commit, 511 + &did, 512 512 verification_channel, 513 513 recipient, 514 514 ) ··· 518 518 super::provision::enqueue_migration_verification( 519 519 &state, 520 520 user_id, 521 - &did_for_commit, 521 + &did, 522 522 verification_channel, 523 523 recipient, 524 524 )
+3 -7
crates/tranquil-api/src/identity/did.rs
··· 44 44 if handle_str.is_empty() { 45 45 return ApiError::InvalidRequest("handle is required".into()).into_response(); 46 46 } 47 - let cache_key = tranquil_pds::cache_keys::handle_key(handle_str); 48 - if let Some(did) = state.cache.get(&cache_key).await { 49 - return DidResponse::response(did).into_response(); 50 - } 51 47 let handle: Handle = match handle_str.parse() { 52 48 Ok(h) => h, 53 49 Err(_) => { ··· 163 159 164 160 async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response { 165 161 let encoded_handle = handle.replace(':', "%3A"); 166 - let expected_did = format!("did:web:{}", encoded_handle); 167 - let expected_did_typed: tranquil_pds::types::Did = match expected_did.parse() { 162 + let expected_did: tranquil_pds::types::Did = match format!("did:web:{}", encoded_handle).parse() 163 + { 168 164 Ok(d) => d, 169 165 Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(), 170 166 }; ··· 350 346 did: &str, 351 347 hostname: &str, 352 348 handle: &str, 353 - expected_signing_key: Option<&str>, 349 + expected_signing_key: Option<&Did>, 354 350 ) -> Result<(), DidWebVerifyError> { 355 351 let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname); 356 352 let subdomain_host = format!("{}.{}", handle, hostname_for_handles);
+2 -2
crates/tranquil-api/src/identity/provision.rs
··· 38 38 state: &AppState, 39 39 signing_key: &SigningKey, 40 40 handle: &Handle, 41 - ) -> Result<String, ApiError> { 41 + ) -> Result<Did, ApiError> { 42 42 let hostname = &tranquil_config::get().server.hostname; 43 43 let pds_endpoint = format!("https://{}", hostname); 44 44 ··· 122 122 123 123 pub async fn resolve_signing_key( 124 124 state: &AppState, 125 - signing_key_did: Option<&str>, 125 + signing_key_did: Option<&Did>, 126 126 ) -> Result<SigningKeyResult, ApiError> { 127 127 match signing_key_did { 128 128 Some(key_did) => {
+14 -5
crates/tranquil-api/src/moderation/mod.rs
··· 66 66 67 67 struct ReportServiceConfig { 68 68 url: String, 69 - did: String, 69 + did: Did, 70 70 } 71 71 72 72 fn get_report_service_config() -> Option<ReportServiceConfig> { 73 73 let cfg = tranquil_config::get(); 74 74 let url = cfg.moderation.report_service_url.clone()?; 75 - let did = cfg.moderation.report_service_did.clone()?; 76 - if url.is_empty() || did.is_empty() { 75 + let did_str = cfg.moderation.report_service_did.as_deref()?; 76 + if url.is_empty() || did_str.is_empty() { 77 77 return None; 78 78 } 79 - Some(ReportServiceConfig { url, did }) 79 + match did_str.parse::<Did>() { 80 + Ok(did) => Some(ReportServiceConfig { url, did }), 81 + Err(_) => { 82 + warn!( 83 + report_service_did = did_str, 84 + "invalid report_service_did, handling reports locally" 85 + ); 86 + None 87 + } 88 + } 80 89 } 81 90 82 91 pub async fn create_report( ··· 97 106 state: &AppState, 98 107 auth_user: &tranquil_pds::auth::AuthenticatedUser, 99 108 service_url: &str, 100 - service_did: &str, 109 + service_did: &Did, 101 110 input: &CreateReportInput, 102 111 ) -> Response { 103 112 if let Err(e) = is_ssrf_safe(service_url) {
+6 -11
crates/tranquil-api/src/repo/import.rs
··· 221 221 })?; 222 222 let new_rev = Tid::now(LimitedU32::MIN); 223 223 let new_rev_str = new_rev.to_string(); 224 - let (commit_bytes, _sig) = create_signed_commit( 225 - did, 226 - import_result.data_cid, 227 - &new_rev_str, 228 - None, 229 - &signing_key, 230 - ) 231 - .map_err(|e| { 232 - error!("Failed to create new commit: {}", e); 233 - ApiError::InternalError(None) 234 - })?; 224 + let (commit_bytes, _sig) = 225 + create_signed_commit(did, import_result.data_cid, &new_rev, None, &signing_key) 226 + .map_err(|e| { 227 + error!("Failed to create new commit: {}", e); 228 + ApiError::InternalError(None) 229 + })?; 235 230 let new_root_cid: cid::Cid = 236 231 state.block_store.put(&commit_bytes).await.map_err(|e| { 237 232 error!("Failed to store new commit block: {:?}", e);
+2 -2
crates/tranquil-api/src/server/account_status.rs
··· 133 133 if did.as_str().starts_with("did:plc:") { 134 134 let max_attempts = if with_retry { 5 } else { 1 }; 135 135 let cache_for_retry = cache.clone(); 136 - let did_owned = did.as_str().to_string(); 136 + let did_owned = did.clone(); 137 137 let expected_owned = expected_endpoint.clone(); 138 138 let attempt_counter = Arc::new(AtomicUsize::new(0)); 139 139 ··· 387 387 .cache 388 388 .delete(&tranquil_pds::cache_keys::plc_data_key(&did)) 389 389 .await; 390 - if state.did_resolver.refresh_did(did.as_str()).await.is_err() { 390 + if state.did_resolver.refresh_did(&did).await.is_err() { 391 391 warn!( 392 392 "[MIGRATION] activateAccount: Failed to refresh DID cache for {}", 393 393 did
+5 -4
crates/tranquil-api/src/server/email.rs
··· 19 19 use tranquil_pds::oauth::scopes::{AccountAction, AccountAttr}; 20 20 use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit}; 21 21 use tranquil_pds::state::AppState; 22 + use tranquil_pds::types::{AtIdentifier, Did}; 22 23 23 24 const EMAIL_UPDATE_TTL: Duration = Duration::from_secs(30 * 60); 24 25 ··· 37 38 38 39 async fn get_pending_email_update( 39 40 cache: &dyn tranquil_pds::cache::Cache, 40 - did: &str, 41 + did: &Did, 41 42 ) -> Option<PendingEmailUpdate> { 42 43 cache 43 44 .get(&tranquil_pds::cache_keys::email_update_key(did)) ··· 79 80 if token_required { 80 81 let token = tranquil_pds::auth::email_token::create_email_token( 81 82 state.cache.as_ref(), 82 - auth.did.as_str(), 83 + &auth.did, 83 84 tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail, 84 85 ) 85 86 .await ··· 249 250 250 251 tranquil_pds::auth::email_token::validate_email_token( 251 252 state.cache.as_ref(), 252 - did.as_str(), 253 + did, 253 254 tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail, 254 255 token, 255 256 ) ··· 298 299 299 300 let short_token_result = tranquil_pds::auth::email_token::validate_email_token( 300 301 state.cache.as_ref(), 301 - did.as_str(), 302 + did, 302 303 tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail, 303 304 token, 304 305 )
+2 -2
crates/tranquil-api/src/server/invite.rs
··· 72 72 73 73 #[derive(Serialize)] 74 74 pub struct AccountCodes { 75 - pub account: String, 76 - pub codes: Vec<String>, 75 + pub account: Did, 76 + pub codes: Vec<InviteCodeValue>, 77 77 } 78 78 79 79 pub async fn create_invite_codes(
+7 -11
crates/tranquil-api/src/server/passkey_account.rs
··· 39 39 pub invite_code: Option<String>, 40 40 pub did: Option<String>, 41 41 pub did_type: Option<String>, 42 - pub signing_key: Option<String>, 42 + pub signing_key: Option<Did>, 43 43 pub verification_channel: Option<tranquil_db_traits::CommsChannel>, 44 44 pub discord_username: Option<String>, 45 45 pub telegram_username: Option<String>, ··· 154 154 let secret_key = key_result.signing_key; 155 155 let reserved_key_id = key_result.reserved_key_id; 156 156 157 - let did = match did_type { 157 + let did: Did = match did_type { 158 158 "web" => { 159 159 let self_hosted_did = match common::create_self_hosted_did_web(&handle) { 160 160 Ok(d) => d, ··· 269 269 None 270 270 }; 271 271 272 - let did_typed: Did = match did.parse() { 273 - Ok(d) => d, 274 - Err(_) => return Err(ApiError::InternalError(Some("Invalid DID".into()))), 275 - }; 276 272 let repo = match crate::identity::provision::init_genesis_repo( 277 273 &state, 278 - &did_typed, 274 + &did, 279 275 &secret_key, 280 276 &secret_key_bytes, 281 277 ) ··· 303 299 let create_input = tranquil_db_traits::CreatePasskeyAccountInput { 304 300 handle: handle.clone(), 305 301 email: email.clone().unwrap_or_default(), 306 - did: did_typed.clone(), 302 + did: did.clone(), 307 303 preferred_comms_channel: verification_channel, 308 304 discord_username: comms.discord, 309 305 telegram_username: comms.telegram, ··· 353 349 crate::identity::provision::enqueue_signup_verification( 354 350 &state, 355 351 user_id, 356 - &did_typed, 352 + &did, 357 353 verification_channel, 358 354 &verification_recipient, 359 355 ) ··· 367 363 let refresh_jti = Jti::from(uuid::Uuid::new_v4().to_string()); 368 364 let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24); 369 365 let session_data = tranquil_db_traits::SessionTokenCreate { 370 - did: did_typed.clone(), 366 + did: did.clone(), 371 367 access_jti: token_meta.jti.clone(), 372 368 refresh_jti, 373 369 access_expires_at: token_meta.expires_at, ··· 688 684 if let Err(e) = state 689 685 .repos 690 686 .user 691 - .set_recovery_token(&user.did, &recovery_token_hash, expires_at) 687 + .set_recovery_token(&user.did, recovery_token_hash.as_str(), expires_at) 692 688 .await 693 689 { 694 690 error!("Error updating recovery token: {:?}", e);
+4 -5
crates/tranquil-api/src/server/session.rs
··· 575 575 &session_row.did, 576 576 &key_bytes, 577 577 session_row.scope.as_deref(), 578 - session_row.controller_did.as_deref(), 578 + session_row.controller_did.as_ref(), 579 579 None, 580 580 ) { 581 581 Ok(m) => m, ··· 747 747 &replay.did, 748 748 key_bytes, 749 749 replay.scope.as_deref(), 750 - replay.controller_did.as_deref(), 750 + replay.controller_did.as_ref(), 751 751 None, 752 752 &replay.access_jti, 753 753 replay.access_expires_at, ··· 914 914 915 915 let session = match crate::identity::provision::create_and_store_session( 916 916 &state, 917 - &row.did, 918 917 &row.did, 919 918 &key_bytes, 920 919 "transition:generic transition:chat.bsky", ··· 957 956 } 958 957 959 958 pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option<AutoResendResult> { 960 - let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did.as_str()); 959 + let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did); 961 960 let debounced = state.cache.get(&debounce_key).await.is_some(); 962 961 let row = match state.repos.user.get_resend_verification_by_did(did).await { 963 962 Ok(Some(row)) => row, ··· 1224 1223 state 1225 1224 .repos 1226 1225 .oauth 1227 - .delete_sessions_by_did_except(&auth.did, &jti_typed) 1226 + .delete_sessions_by_did_except(&auth.did, &token_id) 1228 1227 .await 1229 1228 .log_db_err("revoking OAuth sessions")?; 1230 1229 } else {
+4 -3
crates/tranquil-api/src/server/signing_key.rs
··· 10 10 use tracing::{error, info}; 11 11 use tranquil_pds::api::error::ApiError; 12 12 use tranquil_pds::state::AppState; 13 + use tranquil_pds::types::Did; 13 14 14 15 const SECP256K1_MULTICODEC_PREFIX: [u8; 2] = [0xe7, 0x01]; 15 16 16 - fn public_key_to_did_key(signing_key: &SigningKey) -> String { 17 + fn public_key_to_did_key(signing_key: &SigningKey) -> Did { 17 18 let verifying_key = signing_key.verifying_key(); 18 19 let compressed_pubkey = verifying_key.to_sec1_bytes(); 19 20 let mut multicodec_key = Vec::with_capacity(2 + compressed_pubkey.len()); 20 21 multicodec_key.extend_from_slice(&SECP256K1_MULTICODEC_PREFIX); 21 22 multicodec_key.extend_from_slice(&compressed_pubkey); 22 23 let encoded = multibase::encode(multibase::Base::Base58Btc, &multicodec_key); 23 - format!("did:key:{}", encoded) 24 + Did::from(format!("did:key:{}", encoded)) 24 25 } 25 26 26 27 #[derive(Deserialize)] ··· 31 32 #[derive(Serialize)] 32 33 #[serde(rename_all = "camelCase")] 33 34 pub struct ReserveSigningKeyOutput { 34 - pub signing_key: String, 35 + pub signing_key: Did, 35 36 } 36 37 37 38 pub async fn reserve_signing_key(
+34 -34
crates/tranquil-auth/src/token.rs
··· 12 12 13 13 type HmacSha256 = Hmac<Sha256>; 14 14 15 - pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> { 15 + pub fn create_access_token(did: &Did, key_bytes: &[u8]) -> Result<String> { 16 16 Ok(create_access_token_with_metadata(did, key_bytes)?.token) 17 17 } 18 18 19 - pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> { 19 + pub fn create_refresh_token(did: &Did, key_bytes: &[u8]) -> Result<String> { 20 20 Ok(create_refresh_token_with_metadata(did, key_bytes)?.token) 21 21 } 22 22 23 - pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> { 23 + pub fn create_access_token_with_metadata(did: &Did, key_bytes: &[u8]) -> Result<TokenWithMetadata> { 24 24 create_access_token_with_scope_metadata(did, key_bytes, None, None) 25 25 } 26 26 27 27 pub fn create_access_token_with_scope_metadata( 28 - did: &str, 28 + did: &Did, 29 29 key_bytes: &[u8], 30 30 scopes: Option<&str>, 31 31 hostname: Option<&str>, ··· 42 42 } 43 43 44 44 pub fn create_access_token_with_delegation( 45 - did: &str, 45 + did: &Did, 46 46 key_bytes: &[u8], 47 47 scopes: Option<&str>, 48 - controller_did: Option<&str>, 48 + controller_did: Option<&Did>, 49 49 hostname: Option<&str>, 50 50 ) -> Result<TokenWithMetadata> { 51 51 let scope = scopes.unwrap_or(TokenScope::Access.as_str()); 52 - let act = controller_did.map(|c| ActClaim { sub: c.to_string() }); 52 + let act = controller_did.map(|c| ActClaim { sub: c.clone() }); 53 53 create_signed_token_with_act( 54 54 did, 55 55 scope, ··· 62 62 } 63 63 64 64 pub fn create_refresh_token_with_metadata( 65 - did: &str, 65 + did: &Did, 66 66 key_bytes: &[u8], 67 67 ) -> Result<TokenWithMetadata> { 68 68 create_signed_token_with_metadata( ··· 79 79 /// refresh grace window to reproduce a session's current access token without 80 80 /// persisting the signed JWT itself. 81 81 pub fn create_access_token_with_jti( 82 - did: &str, 82 + did: &Did, 83 83 key_bytes: &[u8], 84 84 scopes: Option<&str>, 85 - controller_did: Option<&str>, 85 + controller_did: Option<&Did>, 86 86 hostname: Option<&str>, 87 87 jti: &Jti, 88 88 expires_at: DateTime<Utc>, 89 89 ) -> Result<String> { 90 90 let scope = scopes.unwrap_or(TokenScope::Access.as_str()); 91 - let act = controller_did.map(|c| ActClaim { sub: c.to_string() }); 91 + let act = controller_did.map(|c| ActClaim { sub: c.clone() }); 92 92 Ok(create_signed_token_pinned( 93 93 did, 94 94 scope, ··· 105 105 /// Re-mint a refresh token carrying a specific `jti` and expiry. Counterpart to 106 106 /// [`create_access_token_with_jti`] for the refresh grace window. 107 107 pub fn create_refresh_token_with_jti( 108 - did: &str, 108 + did: &Did, 109 109 key_bytes: &[u8], 110 110 jti: &Jti, 111 111 expires_at: DateTime<Utc>, ··· 124 124 } 125 125 126 126 pub fn create_service_token( 127 - did: &str, 128 - aud: &str, 127 + did: &Did, 128 + aud: &Did, 129 129 lxm: Option<&Nsid>, 130 130 key_bytes: &[u8], 131 131 ) -> Result<String> { ··· 137 137 .timestamp(); 138 138 139 139 let claims = Claims { 140 - iss: did.to_owned(), 141 - sub: did.to_owned(), 142 - aud: aud.to_owned(), 140 + iss: did.clone(), 141 + sub: did.clone(), 142 + aud: aud.to_string(), 143 143 exp: expiration, 144 144 iat: Utc::now().timestamp(), 145 145 scope: None, ··· 152 152 } 153 153 154 154 fn create_signed_token_with_metadata( 155 - did: &str, 155 + did: &Did, 156 156 scope: &str, 157 157 typ: TokenType, 158 158 key_bytes: &[u8], ··· 163 163 } 164 164 165 165 fn create_signed_token_with_act( 166 - did: &str, 166 + did: &Did, 167 167 scope: &str, 168 168 typ: TokenType, 169 169 key_bytes: &[u8], ··· 180 180 181 181 #[allow(clippy::too_many_arguments)] 182 182 fn create_signed_token_pinned( 183 - did: &str, 183 + did: &Did, 184 184 scope: &str, 185 185 typ: TokenType, 186 186 key_bytes: &[u8], ··· 200 200 }); 201 201 202 202 let claims = Claims { 203 - iss: did.to_owned(), 204 - sub: did.to_owned(), 203 + iss: did.clone(), 204 + sub: did.clone(), 205 205 aud: format!("did:web:{}", aud_hostname), 206 206 exp: expiration, 207 207 iat: Utc::now().timestamp(), ··· 243 243 Ok(format!("{}.{}", message, signature_b64)) 244 244 } 245 245 246 - pub fn create_access_token_hs256(did: &str, secret: &[u8]) -> Result<String> { 246 + pub fn create_access_token_hs256(did: &Did, secret: &[u8]) -> Result<String> { 247 247 Ok(create_access_token_hs256_with_metadata(did, secret)?.token) 248 248 } 249 249 250 - pub fn create_refresh_token_hs256(did: &str, secret: &[u8]) -> Result<String> { 250 + pub fn create_refresh_token_hs256(did: &Did, secret: &[u8]) -> Result<String> { 251 251 Ok(create_refresh_token_hs256_with_metadata(did, secret)?.token) 252 252 } 253 253 254 254 pub fn create_access_token_hs256_with_metadata( 255 - did: &str, 255 + did: &Did, 256 256 secret: &[u8], 257 257 ) -> Result<TokenWithMetadata> { 258 258 create_hs256_token_with_metadata( ··· 265 265 } 266 266 267 267 pub fn create_refresh_token_hs256_with_metadata( 268 - did: &str, 268 + did: &Did, 269 269 secret: &[u8], 270 270 ) -> Result<TokenWithMetadata> { 271 271 create_hs256_token_with_metadata( ··· 278 278 } 279 279 280 280 pub fn create_service_token_hs256( 281 - did: &str, 282 - aud: &str, 281 + did: &Did, 282 + aud: &Did, 283 283 lxm: &Nsid, 284 284 secret: &[u8], 285 285 ) -> Result<String> { ··· 289 289 .timestamp(); 290 290 291 291 let claims = Claims { 292 - iss: did.to_owned(), 293 - sub: did.to_owned(), 294 - aud: aud.to_owned(), 292 + iss: did.clone(), 293 + sub: did.clone(), 294 + aud: aud.to_string(), 295 295 exp: expiration, 296 296 iat: Utc::now().timestamp(), 297 297 scope: None, ··· 304 304 } 305 305 306 306 fn create_hs256_token_with_metadata( 307 - did: &str, 307 + did: &Did, 308 308 scope: &str, 309 309 typ: TokenType, 310 310 secret: &[u8], ··· 318 318 let jti = Jti::new(uuid::Uuid::new_v4().to_string()); 319 319 320 320 let claims = Claims { 321 - iss: did.to_owned(), 322 - sub: did.to_owned(), 321 + iss: did.clone(), 322 + sub: did.clone(), 323 323 aud: format!( 324 324 "did:web:{}", 325 325 tranquil_config::try_get()
+5 -5
crates/tranquil-auth/src/types.rs
··· 204 204 205 205 #[derive(Debug, Clone, Serialize, Deserialize)] 206 206 pub struct ActClaim { 207 - pub sub: String, 207 + pub sub: Did, 208 208 } 209 209 210 210 #[derive(Debug, Serialize, Deserialize)] 211 211 pub struct Claims { 212 - pub iss: String, 213 - pub sub: String, 212 + pub iss: Did, 213 + pub sub: Did, 214 214 pub aud: String, 215 215 pub exp: i64, 216 216 pub iat: i64, ··· 231 231 232 232 #[derive(Debug, Serialize, Deserialize)] 233 233 pub struct UnsafeClaims { 234 - pub iss: String, 235 - pub sub: Option<String>, 234 + pub iss: Did, 235 + pub sub: Option<Did>, 236 236 } 237 237 238 238 pub struct TokenData<T> {
+1 -1
crates/tranquil-auth/src/verify.rs
··· 14 14 15 15 type HmacSha256 = Hmac<Sha256>; 16 16 17 - pub fn get_did_from_token(token: &str) -> Result<String, TokenDecodeError> { 17 + pub fn get_did_from_token(token: &str) -> Result<Did, TokenDecodeError> { 18 18 let parts: Vec<&str> = token.split('.').collect(); 19 19 if parts.len() != 3 { 20 20 return Err(TokenDecodeError::InvalidFormat);
+4 -4
crates/tranquil-db-traits/src/infra.rs
··· 189 189 pub struct ReservedSigningKeyFull { 190 190 pub id: Uuid, 191 191 pub did: Option<Did>, 192 - pub public_key_did_key: String, 192 + pub public_key_did_key: Did, 193 193 pub private_key_bytes: Vec<u8>, 194 194 pub expires_at: DateTime<Utc>, 195 195 pub used_at: Option<DateTime<Utc>>, ··· 314 314 async fn reserve_signing_key( 315 315 &self, 316 316 did: Option<&Did>, 317 - public_key_did_key: &str, 317 + public_key_did_key: &Did, 318 318 private_key_bytes: &[u8], 319 319 expires_at: DateTime<Utc>, 320 320 ) -> Result<Uuid, DbError>; 321 321 322 322 async fn get_reserved_signing_key( 323 323 &self, 324 - public_key_did_key: &str, 324 + public_key_did_key: &Did, 325 325 ) -> Result<Option<ReservedSigningKey>, DbError>; 326 326 327 327 async fn mark_signing_key_used(&self, key_id: Uuid) -> Result<(), DbError>; ··· 455 455 456 456 async fn get_reserved_signing_key_full( 457 457 &self, 458 - public_key_did_key: &str, 458 + public_key_did_key: &Did, 459 459 ) -> Result<Option<ReservedSigningKeyFull>, DbError>; 460 460 461 461 async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError>;
+3 -6
crates/tranquil-db-traits/src/session.rs
··· 233 233 did: &Did, 234 234 ) -> Result<u64, DbError>; 235 235 236 - async fn delete_session_by_id( 237 - &self, 238 - session_id: SessionId, 239 - did: &Did, 240 - ) -> Result<u64, DbError>; 236 + async fn delete_session_by_id(&self, session_id: SessionId, did: &Did) -> Result<u64, DbError>; 241 237 242 238 async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError>; 243 239 ··· 300 296 301 297 async fn update_mfa_verified(&self, did: &Did) -> Result<(), DbError>; 302 298 303 - async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError>; 299 + async fn get_app_password_hashes_by_did(&self, did: &Did) 300 + -> Result<Vec<PasswordHash>, DbError>; 304 301 305 302 async fn refresh_session_atomic( 306 303 &self,
+5 -1
crates/tranquil-db-traits/src/user.rs
··· 224 224 225 225 async fn admin_update_handle(&self, did: &Did, handle: &Handle) -> Result<u64, DbError>; 226 226 227 - async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>; 227 + async fn admin_update_password( 228 + &self, 229 + did: &Did, 230 + password_hash: &PasswordHash, 231 + ) -> Result<u64, DbError>; 228 232 229 233 async fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), DbError>; 230 234
+4 -4
crates/tranquil-db/src/postgres/infra.rs
··· 531 531 async fn reserve_signing_key( 532 532 &self, 533 533 did: Option<&Did>, 534 - public_key_did_key: &str, 534 + public_key_did_key: &Did, 535 535 private_key_bytes: &[u8], 536 536 expires_at: DateTime<Utc>, 537 537 ) -> Result<Uuid, DbError> { ··· 554 554 555 555 async fn get_reserved_signing_key( 556 556 &self, 557 - public_key_did_key: &str, 557 + public_key_did_key: &Did, 558 558 ) -> Result<Option<ReservedSigningKey>, DbError> { 559 559 let result = sqlx::query!( 560 560 r#"SELECT id, private_key_bytes ··· 1123 1123 1124 1124 async fn get_reserved_signing_key_full( 1125 1125 &self, 1126 - public_key_did_key: &str, 1126 + public_key_did_key: &Did, 1127 1127 ) -> Result<Option<ReservedSigningKeyFull>, DbError> { 1128 1128 let row = sqlx::query!( 1129 1129 r#"SELECT id, did, public_key_did_key, private_key_bytes, expires_at, used_at ··· 1137 1137 Ok(row.map(|r| ReservedSigningKeyFull { 1138 1138 id: r.id, 1139 1139 did: r.did.map(|d| Did::new(d).expect("valid DID in database")), 1140 - public_key_did_key: r.public_key_did_key, 1140 + public_key_did_key: Did::from(r.public_key_did_key), 1141 1141 private_key_bytes: r.private_key_bytes, 1142 1142 expires_at: r.expires_at, 1143 1143 used_at: r.used_at,
+5 -6
crates/tranquil-db/src/postgres/session.rs
··· 131 131 Ok(result.rows_affected()) 132 132 } 133 133 134 - async fn delete_session_by_id( 135 - &self, 136 - session_id: SessionId, 137 - did: &Did, 138 - ) -> Result<u64, DbError> { 134 + async fn delete_session_by_id(&self, session_id: SessionId, did: &Did) -> Result<u64, DbError> { 139 135 let result = sqlx::query!( 140 136 "DELETE FROM session_tokens WHERE id = $1 AND did = $2", 141 137 session_id.as_i32(), ··· 499 495 Ok(()) 500 496 } 501 497 502 - async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError> { 498 + async fn get_app_password_hashes_by_did( 499 + &self, 500 + did: &Did, 501 + ) -> Result<Vec<PasswordHash>, DbError> { 503 502 let rows = sqlx::query_scalar!( 504 503 r#"SELECT ap.password_hash FROM app_passwords ap 505 504 JOIN users u ON ap.user_id = u.id
+5 -1
crates/tranquil-db/src/postgres/user.rs
··· 651 651 Ok(result.rows_affected()) 652 652 } 653 653 654 - async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError> { 654 + async fn admin_update_password( 655 + &self, 656 + did: &Did, 657 + password_hash: &PasswordHash, 658 + ) -> Result<u64, DbError> { 655 659 let result = sqlx::query!( 656 660 "UPDATE users SET password_hash = $1 WHERE did = $2", 657 661 password_hash.as_str(),
+15 -18
crates/tranquil-lexicon/src/resolve.rs
··· 60 60 #[error("no DID found in DNS TXT records for {domain}")] 61 61 NoDid { domain: String }, 62 62 #[error("DID document fetch failed for {did}: {reason}")] 63 - DidResolution { did: String, reason: String }, 63 + DidResolution { did: Did, reason: String }, 64 64 #[error("no PDS endpoint found in DID document for {did}")] 65 - NoPdsEndpoint { did: String }, 65 + NoPdsEndpoint { did: Did }, 66 66 #[error("schema fetch failed from {url}: {reason}")] 67 67 SchemaFetch { url: String, reason: String }, 68 68 #[error("schema deserialization failed: {0}")] ··· 82 82 Ok(segments.join(".")) 83 83 } 84 84 85 - pub async fn resolve_did_from_dns(authority: &str) -> Result<String, ResolveError> { 85 + pub async fn resolve_did_from_dns(authority: &str) -> Result<Did, ResolveError> { 86 86 let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or_else(|e| { 87 87 tracing::warn!("falling back to default DNS resolvers: {}", e); 88 88 TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()) 89 89 }); 90 90 91 - let extract_did = |lookup: hickory_resolver::lookup::TxtLookup| -> Option<String> { 91 + let extract_did = |lookup: hickory_resolver::lookup::TxtLookup| -> Option<Did> { 92 92 lookup 93 93 .iter() 94 94 .flat_map(|record| record.txt_data()) 95 95 .find_map(|txt| { 96 96 let txt_str = String::from_utf8_lossy(txt); 97 - txt_str.strip_prefix("did=").and_then(|did| { 98 - let did = did.trim(); 99 - did.starts_with("did:").then(|| did.to_string()) 100 - }) 97 + txt_str 98 + .strip_prefix("did=") 99 + .and_then(|did| Did::new(did.trim()).ok()) 101 100 }) 102 101 }; 103 102 ··· 124 123 } 125 124 126 125 pub async fn resolve_pds_endpoint( 127 - did: &str, 126 + did: &Did, 128 127 plc_directory_url: Option<&str>, 129 128 ) -> Result<String, ResolveError> { 130 129 let plc_base = plc_directory_url.unwrap_or(DEFAULT_PLC_DIRECTORY); ··· 137 136 Some(("web", domain)) => format!("https://{}/.well-known/did.json", domain), 138 137 _ => { 139 138 return Err(ResolveError::DidResolution { 140 - did: did.to_string(), 139 + did: did.clone(), 141 140 reason: "unsupported DID method".to_string(), 142 141 }); 143 142 } ··· 148 147 .send() 149 148 .await 150 149 .map_err(|e| ResolveError::DidResolution { 151 - did: did.to_string(), 150 + did: did.clone(), 152 151 reason: e.to_string(), 153 152 })?; 154 153 155 154 let body = read_body_limited(resp, MAX_RESPONSE_BYTES) 156 155 .await 157 156 .map_err(|reason| ResolveError::DidResolution { 158 - did: did.to_string(), 157 + did: did.clone(), 159 158 reason, 160 159 })?; 161 160 162 161 let doc: serde_json::Value = 163 162 serde_json::from_slice(&body).map_err(|e| ResolveError::DidResolution { 164 - did: did.to_string(), 163 + did: did.clone(), 165 164 reason: e.to_string(), 166 165 })?; 167 166 168 - extract_pds_endpoint(&doc).ok_or(ResolveError::NoPdsEndpoint { 169 - did: did.to_string(), 170 - }) 167 + extract_pds_endpoint(&doc).ok_or_else(|| ResolveError::NoPdsEndpoint { did: did.clone() }) 171 168 } 172 169 173 170 fn extract_pds_endpoint(doc: &serde_json::Value) -> Option<String> { ··· 188 185 189 186 pub async fn fetch_schema_from_pds( 190 187 pds_endpoint: &str, 191 - did: &str, 188 + did: &Did, 192 189 nsid: &Nsid, 193 190 ) -> Result<LexiconDoc, ResolveError> { 194 191 let url = format!( ··· 280 277 281 278 pub async fn resolve_lexicon_from_did( 282 279 nsid: &Nsid, 283 - did: &str, 280 + did: &Did, 284 281 plc_directory_url: Option<&str>, 285 282 ) -> Result<LexiconDoc, ResolveError> { 286 283 let pds_endpoint = resolve_pds_endpoint(did, plc_directory_url).await?;
+6 -6
crates/tranquil-lexicon/tests/resolve_integration.rs
··· 71 71 .mount(&plc_server) 72 72 .await; 73 73 74 - let endpoint = resolve_pds_endpoint(did, Some(&plc_server.uri())) 74 + let endpoint = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())) 75 75 .await 76 76 .unwrap(); 77 77 assert_eq!(endpoint, "https://pds.example.com"); ··· 94 94 .mount(&plc_server) 95 95 .await; 96 96 97 - let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await; 97 + let result = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())).await; 98 98 assert!(matches!(result, Err(ResolveError::NoPdsEndpoint { .. }))); 99 99 } 100 100 ··· 109 109 .mount(&plc_server) 110 110 .await; 111 111 112 - let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await; 112 + let result = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())).await; 113 113 assert!(result.is_err()); 114 114 } 115 115 116 116 #[tokio::test] 117 117 async fn test_resolve_pds_endpoint_unsupported_did_method() { 118 - let result = resolve_pds_endpoint("did:key:z6MkTest", None).await; 118 + let result = resolve_pds_endpoint(&"did:key:z6MkTest".parse().unwrap(), None).await; 119 119 assert!(matches!(result, Err(ResolveError::DidResolution { .. }))); 120 120 } 121 121 ··· 146 146 .mount(&plc_server) 147 147 .await; 148 148 149 - let endpoint = resolve_pds_endpoint(did, Some(&plc_server.uri())) 149 + let endpoint = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())) 150 150 .await 151 151 .unwrap(); 152 152 assert_eq!(endpoint, "https://pds.example.com"); ··· 402 402 .mount(&plc_server) 403 403 .await; 404 404 405 - let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await; 405 + let result = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())).await; 406 406 assert!(result.is_err()); 407 407 }
+2 -2
crates/tranquil-oauth-server/src/endpoints/authorize/consent.rs
··· 19 19 pub logo_uri: Option<String>, 20 20 pub scopes: Vec<ScopeInfo>, 21 21 pub show_consent: bool, 22 - pub did: String, 22 + pub did: Did, 23 23 #[serde(skip_serializing_if = "Option::is_none")] 24 24 pub handle: Option<String>, 25 25 #[serde(skip_serializing_if = "Option::is_none")] ··· 256 256 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 257 257 scopes, 258 258 show_consent, 259 - did: did.to_string(), 259 + did: did.clone(), 260 260 handle: account_handle, 261 261 is_delegation, 262 262 controller_did: controller_did_resp,
+1 -5
crates/tranquil-oauth-server/src/endpoints/delegation.rs
··· 224 224 .flatten(); 225 225 226 226 if is_cross_pds || controller_local.is_none() { 227 - let did_doc = match state 228 - .plc_client() 229 - .get_document(controller_did.as_str()) 230 - .await 231 - { 227 + let did_doc = match state.plc_client().get_document(&controller_did).await { 232 228 Ok(doc) => doc, 233 229 Err(_) => { 234 230 return DelegationAuthResponse::err("Failed to resolve controller DID");
+13 -22
crates/tranquil-oauth-server/src/endpoints/token/grants.rs
··· 65 65 { 66 66 return Err(OAuthError::InvalidGrant("client_id mismatch".to_string())); 67 67 } 68 - let did = authorized.did.to_string(); 68 + let did = authorized.did.clone(); 69 69 let client_metadata_cache = ClientMetadataCache::new(3600); 70 70 let client_metadata = client_metadata_cache.get(&authorized.client_id).await?; 71 71 let client_auth = match &request.client_auth { ··· 134 134 let now = Utc::now(); 135 135 136 136 let (raw_scope, controller_did) = if let Some(ref controller) = authorized.controller_did { 137 - let did_parsed: Did = did 138 - .parse() 139 - .map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?; 140 - let controller_parsed: Did = controller 141 - .parse() 142 - .map_err(|_| OAuthError::InvalidRequest("Invalid controller DID format".to_string()))?; 143 137 let grant = state 144 138 .repos 145 139 .delegation 146 - .get_delegation(&did_parsed, &controller_parsed) 140 + .get_delegation(&did, controller) 147 141 .await 148 142 .ok() 149 143 .flatten(); ··· 179 173 &did, 180 174 dpop_jkt.as_deref(), 181 175 final_scope.as_deref(), 182 - controller_did.as_deref(), 176 + controller_did.as_ref(), 183 177 )?; 184 178 let stored_client_auth = authorized.client_auth.unwrap_or(ClientAuth::None); 185 179 let refresh_expiry_days = if matches!(stored_client_auth, ClientAuth::None) { ··· 189 183 }; 190 184 let mut stored_parameters = authorized.parameters.clone(); 191 185 stored_parameters.dpop_jkt = dpop_jkt.clone(); 192 - let did_typed: Did = did 193 - .parse() 194 - .map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?; 195 186 let token_data = TokenData { 196 - did: did_typed, 187 + did: did.clone(), 197 188 token_id: token_id.clone(), 198 189 created_at: now, 199 190 updated_at: now, ··· 295 286 ); 296 287 let dpop_jkt = token_data.parameters.dpop_jkt.as_deref(); 297 288 let access_token = create_access_token_with_delegation( 298 - &token_data.token_id.0, 299 - token_data.did.as_str(), 289 + &token_data.token_id, 290 + &token_data.did, 300 291 dpop_jkt, 301 292 token_data.scope.as_deref(), 302 - token_data.controller_did.as_ref().map(|d| d.as_str()), 293 + token_data.controller_did.as_ref(), 303 294 )?; 304 295 let mut response_headers = HeaderMap::new(); 305 296 let config = AuthConfig::get(); ··· 320 311 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS, 321 312 refresh_token: token_data.current_refresh_token, 322 313 scope: token_data.scope, 323 - sub: Some(token_data.did.to_string()), 314 + sub: Some(token_data.did), 324 315 }), 325 316 )); 326 317 } ··· 409 400 "Refresh token rotated successfully" 410 401 ); 411 402 let access_token = create_access_token_with_delegation( 412 - &token_data.token_id.0, 413 - token_data.did.as_str(), 414 - dpop_jkt.as_deref(), 403 + &token_data.token_id, 404 + &token_data.did, 405 + dpop_jkt.as_ref(), 415 406 token_data.scope.as_deref(), 416 - token_data.controller_did.as_ref().map(|d| d.as_str()), 407 + token_data.controller_did.as_ref(), 417 408 )?; 418 409 let mut response_headers = HeaderMap::new(); 419 410 let config = AuthConfig::get(); ··· 434 425 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS, 435 426 refresh_token: Some(new_refresh_token), 436 427 scope: token_data.scope, 437 - sub: Some(token_data.did.to_string()), 428 + sub: Some(token_data.did), 438 429 }), 439 430 )) 440 431 }
+1 -10
crates/tranquil-oauth-server/src/endpoints/token/helpers.rs
··· 35 35 sub: &tranquil_types::Did, 36 36 dpop_jkt: Option<&tranquil_types::JwkThumbprint>, 37 37 scope: Option<&str>, 38 - ) -> Result<String, OAuthError> { 39 - create_access_token_with_delegation(session_id, sub, dpop_jkt, scope, None) 40 - } 41 - 42 - pub fn create_access_token_with_delegation( 43 - session_id: &str, 44 - sub: &str, 45 - dpop_jkt: Option<&str>, 46 - scope: Option<&str>, 47 - controller_did: Option<&str>, 38 + controller_did: Option<&tranquil_types::Did>, 48 39 ) -> Result<String, OAuthError> { 49 40 use serde_json::json; 50 41 let jti = uuid::Uuid::new_v4().to_string();
+1 -1
crates/tranquil-oauth-server/src/endpoints/token/types.rs
··· 184 184 #[serde(skip_serializing_if = "Option::is_none")] 185 185 pub scope: Option<String>, 186 186 #[serde(skip_serializing_if = "Option::is_none")] 187 - pub sub: Option<String>, 187 + pub sub: Option<tranquil_types::Did>, 188 188 }
+12 -19
crates/tranquil-oauth-server/src/sso_endpoints.rs
··· 839 839 #[derive(Debug, Serialize)] 840 840 #[serde(rename_all = "camelCase")] 841 841 pub struct CompleteRegistrationResponse { 842 - pub did: String, 842 + pub did: tranquil_pds::types::Did, 843 843 pub handle: tranquil_pds::types::Handle, 844 844 pub redirect_url: String, 845 845 #[serde(skip_serializing_if = "Option::is_none")] ··· 1016 1016 let pds_endpoint = format!("https://{}", hostname); 1017 1017 let did_type = input.did_type.as_deref().unwrap_or("plc"); 1018 1018 1019 - let did = match did_type { 1019 + let did: tranquil_pds::types::Did = match did_type { 1020 1020 "web" => { 1021 1021 if !tranquil_pds::util::is_self_hosted_did_web_enabled() { 1022 1022 return Err(ApiError::SelfHostedDidWebDisabled); 1023 1023 } 1024 1024 let encoded_handle = handle.replace(':', "%3A"); 1025 - let self_hosted_did = format!("did:web:{}", encoded_handle); 1025 + let self_hosted_did = 1026 + tranquil_pds::types::Did::from(format!("did:web:{}", encoded_handle)); 1026 1027 tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account"); 1027 1028 self_hosted_did 1028 1029 } ··· 1094 1095 }; 1095 1096 1096 1097 let rev = Tid::now(LimitedU32::MIN); 1097 - let did_typed: tranquil_pds::types::Did = did 1098 - .parse() 1099 - .map_err(|_| ApiError::InternalError(Some("Invalid DID".into())))?; 1100 1098 let (commit_bytes, _sig) = match tranquil_pds::repo_ops::create_signed_commit( 1101 - &did_typed, 1099 + &did, 1102 1100 mst_root, 1103 1101 &rev, 1104 1102 None, ··· 1133 1131 let create_input = tranquil_db_traits::CreateSsoAccountInput { 1134 1132 handle: handle.clone(), 1135 1133 email: email.clone(), 1136 - did: did_typed.clone(), 1134 + did: did.clone(), 1137 1135 preferred_comms_channel: verification_channel, 1138 1136 discord_username: input 1139 1137 .discord_username ··· 1200 1198 } 1201 1199 if let Err(e) = tranquil_pds::repo_ops::sequence_account_event( 1202 1200 &state, 1203 - &did_typed, 1201 + &did, 1204 1202 tranquil_db_traits::AccountStatus::Active, 1205 1203 ) 1206 1204 .await ··· 1214 1212 }); 1215 1213 if let Err(e) = tranquil_pds::repo_ops::create_record_internal( 1216 1214 &state, 1217 - &did_typed, 1215 + &did, 1218 1216 &tranquil_pds::types::PROFILE_COLLECTION, 1219 1217 &tranquil_pds::types::PROFILE_RKEY, 1220 1218 &profile_record, ··· 1269 1267 "SSO registration completed successfully" 1270 1268 ); 1271 1269 1272 - let user_id = state 1273 - .repos 1274 - .user 1275 - .get_id_by_did(&did_typed) 1276 - .await 1277 - .unwrap_or(None); 1270 + let user_id = state.repos.user.get_id_by_did(&did).await.unwrap_or(None); 1278 1271 1279 1272 let channel_auto_verified = verification_channel == tranquil_db_traits::CommsChannel::Email 1280 1273 && pending_preview.provider_email_verified ··· 1284 1277 let _ = state 1285 1278 .repos 1286 1279 .user 1287 - .set_channel_verified(&did_typed, tranquil_db_traits::CommsChannel::Email) 1280 + .set_channel_verified(&did, tranquil_db_traits::CommsChannel::Email) 1288 1281 .await; 1289 1282 tracing::info!(did = %did, "Auto-verified email from SSO provider"); 1290 1283 ··· 1318 1311 }; 1319 1312 1320 1313 let session_data = tranquil_db_traits::SessionTokenCreate { 1321 - did: did_typed.clone(), 1314 + did: did.clone(), 1322 1315 access_jti: access_meta.jti.clone(), 1323 1316 refresh_jti: refresh_meta.jti.clone(), 1324 1317 access_expires_at: access_meta.expires_at, ··· 1373 1366 1374 1367 if let Some(uid) = user_id { 1375 1368 let verification_token = tranquil_pds::auth::verification_token::generate_signup_token( 1376 - &did_typed, 1369 + &did, 1377 1370 verification_channel, 1378 1371 &verification_recipient, 1379 1372 );
+1 -1
crates/tranquil-oauth/src/types.rs
··· 257 257 #[serde(skip_serializing_if = "Option::is_none")] 258 258 pub scope: Option<String>, 259 259 #[serde(skip_serializing_if = "Option::is_none")] 260 - pub sub: Option<String>, 260 + pub sub: Option<Did>, 261 261 } 262 262 263 263 #[derive(Debug, Clone, Serialize, Deserialize)]
+11 -4
crates/tranquil-pds/src/api/proxy.rs
··· 111 111 } 112 112 113 113 /// Fetch the `feed` generator record from the AppView and return its `did`. 114 - async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> Option<String> { 114 + async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> Option<Did> { 115 115 #[derive(serde::Deserialize)] 116 116 struct GetFeedQuery { 117 117 feed: String, ··· 136 136 return None; 137 137 } 138 138 let body: serde_json::Value = resp.json().await.ok()?; 139 - body.get("value")?.get("did")?.as_str().map(str::to_string) 139 + body.get("value")? 140 + .get("did")? 141 + .as_str() 142 + .and_then(|s| Did::new(s).ok()) 140 143 } 141 144 142 145 pub struct XrpcProxyLayer { ··· 241 244 ) 242 245 .into_response(); 243 246 }; 244 - let Ok(resolved) = state.did_resolver.resolve_service(did, service_id).await else { 247 + let Ok(did) = did.parse::<Did>() else { 248 + return ApiError::InvalidRequest("Invalid DID in atproto-proxy header".into()) 249 + .into_response(); 250 + }; 251 + let Ok(resolved) = state.did_resolver.resolve_service(&did, service_id).await else { 245 252 error!(did = %did, service_id = %service_id, "Could not resolve service DID"); 246 253 return ApiError::UpstreamFailure.into_response(); 247 254 }; ··· 340 347 } 341 348 } 342 349 } else { 343 - (resolved.did.clone(), method) 350 + (resolved.did.clone(), method_nsid.clone()) 344 351 }; 345 352 346 353 match crate::auth::create_service_token(
+28 -27
crates/tranquil-pds/src/auth/email_token.rs
··· 2 2 use std::time::Duration; 3 3 4 4 use crate::cache::Cache; 5 + use crate::types::Did; 5 6 use crate::util::{generate_token_code, normalize_token_code}; 6 7 7 8 const TOKEN_TTL_SECS: u64 = 900; ··· 41 42 ExpiredToken, 42 43 } 43 44 44 - fn cache_key(did: &str, purpose: EmailTokenPurpose) -> String { 45 + fn cache_key(did: &Did, purpose: EmailTokenPurpose) -> String { 45 46 format!("email_token:{}:{}", purpose.as_str(), did) 46 47 } 47 48 ··· 51 52 52 53 pub async fn create_email_token( 53 54 cache: &dyn Cache, 54 - did: &str, 55 + did: &Did, 55 56 purpose: EmailTokenPurpose, 56 57 ) -> Result<String, TokenError> { 57 58 if !cache.is_available() { ··· 80 81 81 82 pub async fn validate_email_token( 82 83 cache: &dyn Cache, 83 - did: &str, 84 + did: &Did, 84 85 purpose: EmailTokenPurpose, 85 86 token: &str, 86 87 ) -> Result<(), TokenError> { ··· 110 111 Ok(()) 111 112 } 112 113 113 - pub async fn delete_email_token(cache: &dyn Cache, did: &str, purpose: EmailTokenPurpose) { 114 + pub async fn delete_email_token(cache: &dyn Cache, did: &Did, purpose: EmailTokenPurpose) { 114 115 let _ = cache.delete(&cache_key(did, purpose)).await; 115 116 } 116 117 ··· 188 189 #[tokio::test] 189 190 async fn test_create_and_validate_token() { 190 191 let cache = MockCache::new(); 191 - let did = "did:plc:test123"; 192 + let did = Did::from("did:plc:teq".to_string()); 192 193 193 - let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail) 194 + let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) 194 195 .await 195 196 .unwrap(); 196 197 ··· 198 199 assert!(token.contains('-')); 199 200 200 201 let result = 201 - validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &token).await; 202 + validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &token).await; 202 203 assert!(result.is_ok()); 203 204 } 204 205 205 206 #[tokio::test] 206 207 async fn test_token_consumed_after_use() { 207 208 let cache = MockCache::new(); 208 - let did = "did:plc:test123"; 209 + let did = Did::from("did:plc:teq".to_string()); 209 210 210 - let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail) 211 + let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) 211 212 .await 212 213 .unwrap(); 213 214 214 - validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &token) 215 + validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &token) 215 216 .await 216 217 .unwrap(); 217 218 218 219 let result = 219 - validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &token).await; 220 + validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &token).await; 220 221 assert_eq!(result.unwrap_err(), TokenError::InvalidToken); 221 222 } 222 223 223 224 #[tokio::test] 224 225 async fn test_invalid_token_rejected() { 225 226 let cache = MockCache::new(); 226 - let did = "did:plc:test123"; 227 + let did = Did::from("did:plc:teq".to_string()); 227 228 228 - let _token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail) 229 + let _token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) 229 230 .await 230 231 .unwrap(); 231 232 232 233 let result = 233 - validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, "XXXXX-XXXXX").await; 234 + validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, "XXXXX-XXXXX").await; 234 235 assert_eq!(result.unwrap_err(), TokenError::InvalidToken); 235 236 } 236 237 237 238 #[tokio::test] 238 239 async fn test_wrong_purpose_rejected() { 239 240 let cache = MockCache::new(); 240 - let did = "did:plc:test123"; 241 + let did = Did::from("did:plc:teq".to_string()); 241 242 242 - let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail) 243 + let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) 243 244 .await 244 245 .unwrap(); 245 246 246 247 let result = 247 - validate_email_token(&cache, did, EmailTokenPurpose::ConfirmEmail, &token).await; 248 + validate_email_token(&cache, &did, EmailTokenPurpose::ConfirmEmail, &token).await; 248 249 assert_eq!(result.unwrap_err(), TokenError::InvalidToken); 249 250 } 250 251 ··· 252 253 async fn test_token_format() { 253 254 // The emitted token is the display form: uppercase `XXXXX-XXXXX`. 254 255 let cache = MockCache::new(); 255 - let did = "did:plc:test123"; 256 + let did = Did::from("did:plc:teq".to_string()); 256 257 (0..50).for_each(|_| { 257 258 let token = futures::executor::block_on(create_email_token( 258 259 &cache, 259 - did, 260 + &did, 260 261 EmailTokenPurpose::UpdateEmail, 261 262 )) 262 263 .unwrap(); ··· 269 270 #[tokio::test] 270 271 async fn test_case_insensitive_validation() { 271 272 let cache = MockCache::new(); 272 - let did = "did:plc:test123"; 273 + let did = Did::from("did:plc:teq".to_string()); 273 274 274 - let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail) 275 + let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) 275 276 .await 276 277 .unwrap(); 277 278 278 279 let lowercase = token.to_lowercase(); 279 280 let result = 280 - validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &lowercase).await; 281 + validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &lowercase).await; 281 282 assert!(result.is_ok()); 282 283 } 283 284 284 285 #[tokio::test] 285 286 async fn test_hyphen_insensitive_validation() { 286 287 let cache = MockCache::new(); 287 - let did = "did:plc:test123"; 288 + let did = Did::from("did:plc:teq".to_string()); 288 289 289 - let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail) 290 + let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) 290 291 .await 291 292 .unwrap(); 292 293 293 294 let no_hyphen = token.replace('-', ""); 294 295 let result = 295 - validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &no_hyphen).await; 296 + validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &no_hyphen).await; 296 297 assert!(result.is_ok()); 297 298 } 298 299 299 300 #[tokio::test] 300 301 async fn test_noop_cache_returns_unavailable() { 301 302 let cache = crate::cache::NoOpCache; 302 - let did = "did:plc:test"; 303 + let did = Did::from("did:plc:whelk".to_string()); 303 304 304 - let result = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail).await; 305 + let result = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail).await; 305 306 assert_eq!(result.unwrap_err(), TokenError::CacheUnavailable); 306 307 } 307 308 }
+8 -8
crates/tranquil-pds/src/auth/legacy_2fa.rs
··· 58 58 } 59 59 60 60 pub async fn clear_challenge(cache: &dyn Cache, did: &Did) { 61 - let _ = cache.delete(&challenge_key(did.as_str())).await; 62 - let _ = cache.delete(&cooldown_key(did.as_str())).await; 61 + let _ = cache.delete(&challenge_key(did)).await; 62 + let _ = cache.delete(&cooldown_key(did)).await; 63 63 } 64 64 65 65 async fn validate_challenge_internal( 66 66 cache: &dyn Cache, 67 - did: &str, 67 + did: &Did, 68 68 code: &str, 69 69 ) -> Result<(), ValidationError> { 70 70 if !cache.is_available() { ··· 119 119 Ok(()) 120 120 } 121 121 122 - fn challenge_key(did: &str) -> String { 122 + fn challenge_key(did: &Did) -> String { 123 123 format!("legacy_2fa:{}", did) 124 124 } 125 125 126 - fn cooldown_key(did: &str) -> String { 126 + fn cooldown_key(did: &Did) -> String { 127 127 format!("legacy_2fa_cooldown:{}", did) 128 128 } 129 129 ··· 215 215 did: &Did, 216 216 code: &str, 217 217 ) -> Result<(), ValidationError> { 218 - validate_challenge_internal(cache, did.as_str(), code).await 218 + validate_challenge_internal(cache, did, code).await 219 219 } 220 220 221 221 async fn create_challenge_code( ··· 226 226 return Err(ChallengeError::CacheUnavailable); 227 227 } 228 228 229 - let cooldown = cooldown_key(did.as_str()); 229 + let cooldown = cooldown_key(did); 230 230 if cache.get(&cooldown).await.is_some() { 231 231 return Err(ChallengeError::RateLimited); 232 232 } ··· 244 244 245 245 cache 246 246 .set( 247 - &challenge_key(did.as_str()), 247 + &challenge_key(did), 248 248 &json, 249 249 Duration::from_secs(CHALLENGE_TTL_SECS), 250 250 )
+4 -13
crates/tranquil-pds/src/auth/mod.rs
··· 372 372 ) 373 373 .await; 374 374 375 - let status_cache_key = crate::cache_keys::user_status_key(did.as_ref()); 375 + let status_cache_key = crate::cache_keys::user_status_key(&did); 376 376 let cached = CachedUserStatus { 377 377 deactivated: user.deactivated_at.is_some(), 378 378 takendown: user.takedown_ref.is_some(), ··· 504 504 oauth_token.key_bytes.as_deref(), 505 505 oauth_token.encryption_version, 506 506 ); 507 - let did: Did = oauth_token 508 - .did 509 - .parse() 510 - .map_err(|_| TokenValidationError::InvalidToken)?; 511 - let controller_did: Option<Did> = oauth_info 512 - .controller_did 513 - .map(|d| d.parse()) 514 - .transpose() 515 - .map_err(|_| TokenValidationError::InvalidToken)?; 516 507 return Ok(AuthenticatedUser { 517 - did, 508 + did: oauth_token.did, 518 509 key_bytes, 519 510 is_admin: oauth_token.is_admin, 520 511 status, 521 512 scope: oauth_info.scope, 522 - controller_did, 513 + controller_did: oauth_info.controller_did, 523 514 auth_source: AuthSource::OAuth, 524 515 }); 525 516 } else { ··· 530 521 Err(TokenValidationError::AuthenticationFailed) 531 522 } 532 523 533 - pub async fn invalidate_auth_cache(cache: &dyn Cache, did: &str) { 524 + pub async fn invalidate_auth_cache(cache: &dyn Cache, did: &Did) { 534 525 let key_cache_key = crate::cache_keys::signing_key_key(did); 535 526 let status_cache_key = crate::cache_keys::user_status_key(did); 536 527 let _ = cache.delete(&key_cache_key).await;
+12 -8
crates/tranquil-pds/src/auth/service.rs
··· 219 219 } 220 220 } 221 221 222 - let did = claims.iss.as_str(); 222 + let did = &claims.iss; 223 223 let public_key = self.resolve_signing_key(did).await?; 224 224 225 225 let signature_bytes = URL_SAFE_NO_PAD ··· 240 240 Ok(claims) 241 241 } 242 242 243 - async fn resolve_signing_key(&self, did: &str) -> Result<VerifyingKey, ServiceTokenError> { 243 + async fn resolve_signing_key(&self, did: &Did) -> Result<VerifyingKey, ServiceTokenError> { 244 244 let did_doc = self.resolve_did_document(did).await?; 245 245 246 246 let atproto_key = did_doc ··· 257 257 parse_did_key_multibase(multibase) 258 258 } 259 259 260 - async fn resolve_did_document(&self, did: &str) -> Result<FullDidDocument, ServiceTokenError> { 261 - if did.starts_with("did:plc:") { 260 + async fn resolve_did_document(&self, did: &Did) -> Result<FullDidDocument, ServiceTokenError> { 261 + if did.is_plc() { 262 262 self.resolve_did_plc(did).await 263 - } else if did.starts_with("did:web:") { 263 + } else if did.is_web() { 264 264 self.resolve_did_web(did).await 265 265 } else { 266 266 Err(ServiceTokenError::UnsupportedDidMethod) 267 267 } 268 268 } 269 269 270 - async fn resolve_did_plc(&self, did: &str) -> Result<FullDidDocument, ServiceTokenError> { 271 - let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 270 + async fn resolve_did_plc(&self, did: &Did) -> Result<FullDidDocument, ServiceTokenError> { 271 + let url = format!( 272 + "{}/{}", 273 + self.plc_directory_url, 274 + urlencoding::encode(did.as_str()) 275 + ); 272 276 debug!("Resolving did:plc {} via {}", did, url); 273 277 274 278 let resp = self ··· 291 295 .map_err(ServiceTokenError::InvalidDidDocument) 292 296 } 293 297 294 - async fn resolve_did_web(&self, did: &str) -> Result<FullDidDocument, ServiceTokenError> { 298 + async fn resolve_did_web(&self, did: &Did) -> Result<FullDidDocument, ServiceTokenError> { 295 299 let host = did 296 300 .strip_prefix("did:web:") 297 301 .ok_or(ServiceTokenError::InvalidFormat)?;
+7 -7
crates/tranquil-pds/src/cache_keys.rs
··· 4 4 format!("auth:session:{}:{}", did, jti) 5 5 } 6 6 7 - pub fn signing_key_key(did: &str) -> String { 7 + pub fn signing_key_key(did: &Did) -> String { 8 8 format!("auth:key:{}", did) 9 9 } 10 10 11 - pub fn user_status_key(did: &str) -> String { 11 + pub fn user_status_key(did: &Did) -> String { 12 12 format!("auth:status:{}", did) 13 13 } 14 14 ··· 16 16 format!("handle:{}", handle) 17 17 } 18 18 19 - pub fn reauth_key(did: &str) -> String { 19 + pub fn reauth_key(did: &Did) -> String { 20 20 format!("reauth:{}", did) 21 21 } 22 22 23 - pub fn plc_doc_key(did: &str) -> String { 23 + pub fn plc_doc_key(did: &Did) -> String { 24 24 format!("plc:doc:{}", did) 25 25 } 26 26 27 - pub fn plc_data_key(did: &str) -> String { 27 + pub fn plc_data_key(did: &Did) -> String { 28 28 format!("plc:data:{}", did) 29 29 } 30 30 31 - pub fn email_update_key(did: &str) -> String { 31 + pub fn email_update_key(did: &Did) -> String { 32 32 format!("email_update:{}", did) 33 33 } 34 34 ··· 36 36 format!("scope_ref:{}", cid) 37 37 } 38 38 39 - pub fn auto_verify_sent_key(did: &str) -> String { 39 + pub fn auto_verify_sent_key(did: &Did) -> String { 40 40 format!("auto_verify_sent:{}", did) 41 41 }
+1 -1
crates/tranquil-pds/src/delegation/mod.rs
··· 38 38 .flatten() 39 39 .is_some(); 40 40 41 - let did_doc = state.did_resolver.resolve_did(did.as_str()).await?; 41 + let did_doc = state.did_resolver.resolve_did(did).await?; 42 42 43 43 let pds_url = did_doc.services.iter().find_map(|svc| { 44 44 if (svc.id == "#atproto_pds" || svc.id.ends_with("#atproto_pds"))
+39 -30
crates/tranquil-pds/src/did.rs
··· 1 + use crate::types::Did; 1 2 use reqwest::Client; 2 3 use serde::{Deserialize, Serialize}; 3 4 use std::collections::HashMap; ··· 30 31 31 32 #[derive(Debug, Clone, Serialize, Deserialize)] 32 33 pub struct DidDocument { 33 - pub id: String, 34 + pub id: Did, 34 35 #[serde(default)] 35 36 #[serde(rename = "service")] 36 37 pub services: Vec<DidService>, ··· 51 52 #[derive(Debug, Clone)] 52 53 pub struct ResolvedService { 53 54 pub url: String, 54 - pub did: String, 55 + pub did: Did, 55 56 pub service_id: String, 56 57 } 57 58 ··· 94 95 95 96 pub async fn resolve_service( 96 97 &self, 97 - did: &str, 98 + did: &Did, 98 99 service_id: &str, 99 100 ) -> Result<Arc<ResolvedService>, ServiceResolutionError> { 100 101 { ··· 117 118 118 119 let resolved = Arc::new(ResolvedService { 119 120 url: service.service_endpoint.clone(), 120 - did: did.into(), 121 + did: did.clone(), 121 122 service_id: service_id.into(), 122 123 }); 123 124 ··· 132 133 Ok(resolved) 133 134 } 134 135 135 - pub async fn resolve_did(&self, did: &str) -> Result<Arc<DidDocument>, DidResolutionError> { 136 + pub async fn resolve_did(&self, did: &Did) -> Result<Arc<DidDocument>, DidResolutionError> { 136 137 { 137 138 let cache = self.parsed_did_doc_cache.read().await; 138 - if let Some(cached) = cache.get(did) 139 + if let Some(cached) = cache.get(did.as_str()) 139 140 && cached.0.elapsed() < self.cache_ttl 140 141 { 141 142 return Ok(cached.1.clone()); ··· 146 147 147 148 { 148 149 let mut cache = self.parsed_did_doc_cache.write().await; 149 - cache.insert(did.into(), (Instant::now(), resolved.clone())); 150 + cache.insert(did.as_str().into(), (Instant::now(), resolved.clone())); 150 151 } 151 152 152 153 Ok(resolved) 153 154 } 154 155 155 - pub async fn refresh_did(&self, did: &str) -> Result<Arc<DidDocument>, DidResolutionError> { 156 + pub async fn refresh_did(&self, did: &Did) -> Result<Arc<DidDocument>, DidResolutionError> { 156 157 { 157 158 let mut cache = self.parsed_did_doc_cache.write().await; 158 - cache.remove(did); 159 + cache.remove(did.as_str()); 159 160 let mut cache = self.service_cache.write().await; 160 - cache.retain(|k, _| !k.starts_with(did)); 161 + cache.retain(|k, _| !k.starts_with(did.as_str())); 161 162 } 162 163 self.resolve_did(did).await 163 164 } 164 165 165 - async fn resolve_did_uncached(&self, did: &str) -> Result<DidDocument, DidResolutionError> { 166 - if did.starts_with("did:web:") { 166 + async fn resolve_did_uncached(&self, did: &Did) -> Result<DidDocument, DidResolutionError> { 167 + if did.is_web() { 167 168 self.resolve_did_web(did).await 168 - } else if did.starts_with("did:plc:") { 169 + } else if did.is_plc() { 169 170 self.resolve_did_plc(did).await 170 171 } else { 171 172 warn!("Unsupported DID method: {}", did); 172 - Err(DidResolutionError::UnsupportedDidMethod(did.into())) 173 + Err(DidResolutionError::UnsupportedDidMethod(did.to_string())) 173 174 } 174 175 } 175 176 176 - async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, DidResolutionError> { 177 + async fn resolve_did_web(&self, did: &Did) -> Result<DidDocument, DidResolutionError> { 177 178 let url = build_did_web_url(did)?; 178 179 179 180 debug!("Resolving did:web {} via {}", did, url); ··· 197 198 .map_err(|e| DidResolutionError::InvalidDocument(e.to_string())) 198 199 } 199 200 200 - async fn resolve_did_plc(&self, did: &str) -> Result<DidDocument, DidResolutionError> { 201 - let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 201 + async fn resolve_did_plc(&self, did: &Did) -> Result<DidDocument, DidResolutionError> { 202 + let url = format!( 203 + "{}/{}", 204 + self.plc_directory_url, 205 + urlencoding::encode(did.as_str()) 206 + ); 202 207 203 208 debug!("Resolving did:plc {} via {}", did, url); 204 209 ··· 227 232 228 233 pub async fn fetch_did_document( 229 234 &self, 230 - did: &str, 235 + did: &Did, 231 236 ) -> Result<Arc<serde_json::Value>, DidResolutionError> { 232 237 { 233 238 let cache = self.did_doc_cache.read().await; 234 - if let Some(cached) = cache.get(did) 239 + if let Some(cached) = cache.get(did.as_str()) 235 240 && cached.0.elapsed() < self.cache_ttl 236 241 { 237 242 return Ok(cached.1.clone()); ··· 242 247 243 248 { 244 249 let mut cache = self.did_doc_cache.write().await; 245 - cache.insert(did.into(), (Instant::now(), resolved.clone())); 250 + cache.insert(did.as_str().into(), (Instant::now(), resolved.clone())); 246 251 } 247 252 248 253 Ok(resolved) ··· 251 256 // TODO: make cached version 252 257 async fn fetch_did_document_uncached( 253 258 &self, 254 - did: &str, 259 + did: &Did, 255 260 ) -> Result<serde_json::Value, DidResolutionError> { 256 - if did.starts_with("did:web:") { 261 + if did.is_web() { 257 262 self.fetch_did_document_web(did).await 258 - } else if did.starts_with("did:plc:") { 263 + } else if did.is_plc() { 259 264 self.fetch_did_document_plc(did).await 260 265 } else { 261 266 warn!("Unsupported DID method: {}", did); 262 - Err(DidResolutionError::UnsupportedDidMethod(did.into())) 267 + Err(DidResolutionError::UnsupportedDidMethod(did.to_string())) 263 268 } 264 269 } 265 270 266 271 async fn fetch_did_document_web( 267 272 &self, 268 - did: &str, 273 + did: &Did, 269 274 ) -> Result<serde_json::Value, DidResolutionError> { 270 275 let url = build_did_web_url(did)?; 271 276 ··· 290 295 291 296 async fn fetch_did_document_plc( 292 297 &self, 293 - did: &str, 298 + did: &Did, 294 299 ) -> Result<serde_json::Value, DidResolutionError> { 295 - let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 300 + let url = format!( 301 + "{}/{}", 302 + self.plc_directory_url, 303 + urlencoding::encode(did.as_str()) 304 + ); 296 305 297 306 let resp = self 298 307 .client ··· 317 326 .map_err(|e| DidResolutionError::InvalidDocument(e.to_string())) 318 327 } 319 328 320 - pub async fn invalidate_cache(&self, did: &str) { 329 + pub async fn invalidate_cache(&self, did: &Did) { 321 330 let mut doc_cache = self.parsed_did_doc_cache.write().await; 322 - doc_cache.remove(did); 331 + doc_cache.remove(did.as_str()); 323 332 } 324 333 } 325 334 ··· 333 342 Arc::new(DidResolver::new()) 334 343 } 335 344 336 - fn build_did_web_url(did: &str) -> Result<String, DidResolutionError> { 345 + fn build_did_web_url(did: &Did) -> Result<String, DidResolutionError> { 337 346 let host = did 338 347 .strip_prefix("did:web:") 339 348 .ok_or(DidResolutionError::InvalidDidWeb)?;
+6 -12
crates/tranquil-pds/src/handle/mod.rs
··· 16 16 #[error("Invalid DID format in record")] 17 17 InvalidDid, 18 18 #[error("DID mismatch: expected {expected}, got {actual}")] 19 - DidMismatch { expected: String, actual: String }, 19 + DidMismatch { expected: Did, actual: Did }, 20 20 } 21 21 22 22 pub async fn resolve_handle_dns(handle: &Handle) -> Result<Did, HandleResolutionError> { ··· 34 34 .flat_map(|record| record.txt_data()) 35 35 .find_map(|txt| { 36 36 let txt_str = String::from_utf8_lossy(txt); 37 - txt_str.strip_prefix("did=").and_then(|did| { 38 - let did = did.trim(); 39 - did.starts_with("did:").then(|| did.to_string()) 40 - }) 37 + txt_str 38 + .strip_prefix("did=") 39 + .and_then(|did| Did::new(did.trim()).ok()) 41 40 }) 42 41 .ok_or(HandleResolutionError::NotFound) 43 42 } ··· 58 57 .text() 59 58 .await 60 59 .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?; 61 - let did = body.trim(); 62 - if did.starts_with("did:") { 63 - Ok(did.to_string()) 64 - } else { 65 - Err(HandleResolutionError::InvalidDid) 66 - } 60 + Did::new(body.trim()).map_err(|_| HandleResolutionError::InvalidDid) 67 61 } 68 62 69 63 pub async fn resolve_handle(handle: &Handle) -> Result<Did, HandleResolutionError> { ··· 78 72 79 73 pub async fn verify_handle_ownership( 80 74 handle: &Handle, 81 - expected_did: &str, 75 + expected_did: &Did, 82 76 ) -> Result<(), HandleResolutionError> { 83 77 let resolved_did = resolve_handle(handle).await?; 84 78 if resolved_did == expected_did {
+2 -2
crates/tranquil-pds/src/oauth/client.rs
··· 110 110 }) 111 111 } 112 112 113 - pub async fn check_remote_is_delegated(&self, pds_url: &str, did: &str) -> Option<bool> { 113 + pub async fn check_remote_is_delegated(&self, pds_url: &str, did: &Did) -> Option<bool> { 114 114 let url = format!( 115 115 "{}/oauth/security-status?identifier={}", 116 116 pds_url.trim_end_matches('/'), 117 - urlencoding::encode(did) 117 + urlencoding::encode(did.as_str()) 118 118 ); 119 119 let resp = self.http.get(&url).send().await.ok()?; 120 120 if !resp.status().is_success() {
+1 -5
crates/tranquil-pds/src/oauth/verify.rs
··· 93 93 )); 94 94 } 95 95 } 96 - let did: Did = token_data 97 - .did 98 - .parse() 99 - .map_err(|_| OAuthError::InvalidToken("Invalid DID in token".to_string()))?; 100 96 Ok(VerifyResult { 101 - did, 97 + did: token_data.did, 102 98 token_id, 103 99 client_id: token_data.client_id, 104 100 scope: token_data.scope,
+10 -10
crates/tranquil-pds/src/plc/mod.rs
··· 202 202 } 203 203 } 204 204 205 - fn encode_did(did: &str) -> String { 206 - urlencoding::encode(did).to_string() 205 + fn encode_did(did: &Did) -> String { 206 + urlencoding::encode(did.as_str()).to_string() 207 207 } 208 208 209 - pub async fn get_document(&self, did: &str) -> Result<Value, PlcError> { 209 + pub async fn get_document(&self, did: &Did) -> Result<Value, PlcError> { 210 210 let cache_key = crate::cache_keys::plc_doc_key(did); 211 211 if let Some(ref cache) = self.cache 212 212 && let Some(cached) = cache.get(&cache_key).await ··· 245 245 Ok(value) 246 246 } 247 247 248 - pub async fn get_document_data(&self, did: &str) -> Result<Value, PlcError> { 248 + pub async fn get_document_data(&self, did: &Did) -> Result<Value, PlcError> { 249 249 let cache_key = crate::cache_keys::plc_data_key(did); 250 250 if let Some(ref cache) = self.cache 251 251 && let Some(cached) = cache.get(&cache_key).await ··· 284 284 Ok(value) 285 285 } 286 286 287 - pub async fn get_last_op(&self, did: &str) -> Result<PlcOpOrTombstone, PlcError> { 287 + pub async fn get_last_op(&self, did: &Did) -> Result<PlcOpOrTombstone, PlcError> { 288 288 let url = format!("{}/{}/log/last", self.base_url, Self::encode_did(did)); 289 289 let response = self.client.get(&url).send().await?; 290 290 if response.status() == reqwest::StatusCode::NOT_FOUND { ··· 304 304 .map_err(|e| PlcError::InvalidResponse(e.to_string())) 305 305 } 306 306 307 - pub async fn get_audit_log(&self, did: &str) -> Result<Vec<Value>, PlcError> { 307 + pub async fn get_audit_log(&self, did: &Did) -> Result<Vec<Value>, PlcError> { 308 308 let url = format!("{}/{}/log/audit", self.base_url, Self::encode_did(did)); 309 309 let response = self.client.get(&url).send().await?; 310 310 if response.status() == reqwest::StatusCode::NOT_FOUND { ··· 324 324 .map_err(|e| PlcError::InvalidResponse(e.to_string())) 325 325 } 326 326 327 - pub async fn send_operation(&self, did: &str, operation: &Value) -> Result<(), PlcError> { 327 + pub async fn send_operation(&self, did: &Did, operation: &Value) -> Result<(), PlcError> { 328 328 let url = format!("{}/{}", self.base_url, Self::encode_did(did)); 329 329 let response = self.client.post(&url).json(operation).send().await?; 330 330 if !response.status().is_success() { ··· 512 512 } 513 513 514 514 pub struct GenesisResult { 515 - pub did: String, 515 + pub did: Did, 516 516 pub signed_operation: Value, 517 517 } 518 518 ··· 553 553 }) 554 554 } 555 555 556 - pub fn did_for_genesis_op(signed_op: &Value) -> Result<String, PlcError> { 556 + pub fn did_for_genesis_op(signed_op: &Value) -> Result<Did, PlcError> { 557 557 let cbor_bytes = serde_ipld_dagcbor::to_vec(signed_op) 558 558 .map_err(|e| PlcError::Serialization(e.to_string()))?; 559 559 let mut hasher = Sha256::new(); ··· 561 561 let hash = hasher.finalize(); 562 562 let encoded = base32::encode(Alphabet::Rfc4648Lower { padding: false }, &hash); 563 563 let truncated = &encoded[..24]; 564 - Ok(format!("did:plc:{}", truncated)) 564 + Ok(Did::from(format!("did:plc:{}", truncated))) 565 565 } 566 566 567 567 pub fn validate_plc_operation(op: &Value) -> Result<PlcOpType, PlcError> {
+8 -9
crates/tranquil-pds/src/sync/verify.rs
··· 124 124 &self, 125 125 did: &Did, 126 126 ) -> Result<DidDocument<'static>, VerifyError> { 127 - let did_str = did.as_str(); 128 - if did_str.starts_with("did:plc:") { 129 - self.resolve_plc_did(did_str).await 130 - } else if did_str.starts_with("did:web:") { 131 - self.resolve_web_did(did_str).await 127 + if did.is_plc() { 128 + self.resolve_plc_did(did).await 129 + } else if did.is_web() { 130 + self.resolve_web_did(did).await 132 131 } else { 133 132 Err(VerifyError::DidResolutionFailed(format!( 134 133 "Unsupported DID method: {}", 135 - did_str 134 + did 136 135 ))) 137 136 } 138 137 } 139 138 140 - async fn resolve_plc_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 139 + async fn resolve_plc_did(&self, did: &Did) -> Result<DidDocument<'static>, VerifyError> { 141 140 let plc_url = std::env::var("PLC_DIRECTORY_URL") 142 141 .unwrap_or_else(|_| tranquil_config::get().plc.directory_url.clone()); 143 - let url = format!("{}/{}", plc_url, urlencoding::encode(did)); 142 + let url = format!("{}/{}", plc_url, urlencoding::encode(did.as_str())); 144 143 let response = self 145 144 .http_client 146 145 .get(&url) ··· 162 161 Ok(doc.into_static()) 163 162 } 164 163 165 - async fn resolve_web_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> { 164 + async fn resolve_web_did(&self, did: &Did) -> Result<DidDocument<'static>, VerifyError> { 166 165 let domain = did.strip_prefix("did:web:").ok_or_else(|| { 167 166 VerifyError::DidResolutionFailed("Invalid did:web format".to_string()) 168 167 })?;
+6 -6
crates/tranquil-pds/tests/commit_signing.rs
··· 14 14 Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap(); 15 15 let rev = Tid::now(LimitedU32::MIN); 16 16 17 - let did_typed = jacquard_common::types::string::Did::new(did).unwrap(); 18 - let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None); 17 + let did = jacquard_common::types::string::Did::new(did).unwrap(); 18 + let unsigned = Commit::new_unsigned(did, data_cid, rev, None); 19 19 let signed = unsigned.sign(&signing_key).unwrap(); 20 20 21 21 let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true); ··· 38 38 Cid::from_str("bafyreigxmvutyl3k5m4guzwxv3xf34gfxjlykgfdqkjmf32vwb5vcjxlui").unwrap(); 39 39 let rev = Tid::now(LimitedU32::MIN); 40 40 41 - let did_typed = jacquard_common::types::string::Did::new(did).unwrap(); 42 - let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, Some(prev_cid)); 41 + let did = jacquard_common::types::string::Did::new(did).unwrap(); 42 + let unsigned = Commit::new_unsigned(did, data_cid, rev, Some(prev_cid)); 43 43 let signed = unsigned.sign(&signing_key).unwrap(); 44 44 45 45 let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true); ··· 58 58 Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap(); 59 59 let rev = Tid::from_str("3masrxv55po22").unwrap(); 60 60 61 - let did_typed = jacquard_common::types::string::Did::new(did).unwrap(); 62 - let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None); 61 + let did = jacquard_common::types::string::Did::new(did).unwrap(); 62 + let unsigned = Commit::new_unsigned(did, data_cid, rev, None); 63 63 64 64 let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap(); 65 65
+13 -12
crates/tranquil-pds/tests/jwt_security.rs
··· 40 40 #[test] 41 41 fn test_signature_attacks() { 42 42 let key_bytes = generate_user_key(); 43 - let did = "did:plc:test"; 44 - let token = create_access_token(did, &key_bytes).expect("create token"); 43 + let did = Did::from("did:plc:whelk".to_string()); 44 + let token = create_access_token(&did, &key_bytes).expect("create token"); 45 45 let parts: Vec<&str> = token.split('.').collect(); 46 46 47 47 let forged_signature = URL_SAFE_NO_PAD.encode([0u8; 64]); ··· 143 143 #[test] 144 144 fn test_token_type_confusion() { 145 145 let key_bytes = generate_user_key(); 146 - let did = "did:plc:test"; 146 + let did = Did::from("did:plc:whelk".to_string()); 147 147 148 148 let refresh_token = create_refresh_token(&did, &key_bytes).expect("create refresh token"); 149 149 let result = verify_access_token(&refresh_token, &key_bytes); ··· 156 156 .contains("Invalid token type") 157 157 ); 158 158 159 - let access_token = create_access_token(did, &key_bytes).expect("create access token"); 159 + let access_token = create_access_token(&did, &key_bytes).expect("create access token"); 160 160 let result = verify_refresh_token(&access_token, &key_bytes); 161 161 assert!(result.is_err(), "Access token as refresh must be rejected"); 162 162 assert!( ··· 168 168 ); 169 169 170 170 let service_token = create_service_token( 171 - did, 172 - "did:web:target", 173 - Some("com.example.method"), 171 + &did, 172 + &Did::from("did:web:nel.pet".to_string()), 173 + Some(&Nsid::from("cafe.oyster.method".to_string())), 174 174 &key_bytes, 175 175 ) 176 176 .unwrap(); ··· 434 434 #[test] 435 435 fn test_did_and_jti_extraction() { 436 436 let key_bytes = generate_user_key(); 437 - let did = "did:plc:legitimate"; 438 - let token = create_access_token(did, &key_bytes).expect("create token"); 437 + let did = Did::from("did:plc:limpet".to_string()); 438 + let token = create_access_token(&did, &key_bytes).expect("create token"); 439 439 440 440 assert_eq!(get_did_from_token(&token).unwrap(), did); 441 441 assert!(get_did_from_token("invalid").is_err()); ··· 459 459 #[test] 460 460 fn test_header_injection_and_constant_time() { 461 461 let key_bytes = generate_user_key(); 462 - let did = "did:plc:test"; 462 + let did = Did::from("did:plc:whelk".to_string()); 463 463 464 464 let header = json!({ 465 465 "alg": "ES256K", "typ": TokenType::Access.as_str(), ··· 474 474 verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok() 475 475 ); 476 476 477 - let valid_token = create_access_token(did, &key_bytes).expect("create token"); 477 + let valid_token = create_access_token(&did, &key_bytes).expect("create token"); 478 478 let parts: Vec<&str> = valid_token.split('.').collect(); 479 479 let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap(); 480 480 almost_valid[0] ^= 1; ··· 500 500 let http_client = client(); 501 501 502 502 let key_bytes = generate_user_key(); 503 - let forged_token = create_access_token("did:plc:fake-user", &key_bytes).unwrap(); 503 + let forged_token = 504 + create_access_token(&Did::from("did:plc:lyna".to_string()), &key_bytes).unwrap(); 504 505 let res = http_client 505 506 .get(format!("{}/xrpc/com.atproto.server.getSession", url)) 506 507 .header("Authorization", format!("Bearer {}", forged_token))
+3 -3
crates/tranquil-pds/tests/scope_edge_cases.rs
··· 176 176 fn test_permissions_rpc_lxm_wildcard_prefix() { 177 177 let perms = 178 178 ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app")); 179 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 180 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed")); 181 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.actor.getProfile")); 179 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 180 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed"))); 181 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.actor.getProfile"))); 182 182 } 183 183 184 184 #[test]
+3 -3
crates/tranquil-pds/tests/signing_key.rs
··· 48 48 assert!(signing_key.starts_with("did:key:z")); 49 49 let row = repos 50 50 .infra 51 - .get_reserved_signing_key_full(signing_key) 51 + .get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string())) 52 52 .await 53 53 .expect("db error") 54 54 .expect("Reserved key not found in database"); ··· 75 75 let signing_key = body["signingKey"].as_str().unwrap(); 76 76 let row = repos 77 77 .infra 78 - .get_reserved_signing_key_full(signing_key) 78 + .get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string())) 79 79 .await 80 80 .expect("db error") 81 81 .expect("Reserved key not found in database"); ··· 185 185 assert!(!access_jwt.is_empty()); 186 186 let reserved = repos 187 187 .infra 188 - .get_reserved_signing_key_full(signing_key) 188 + .get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string())) 189 189 .await 190 190 .expect("db error") 191 191 .expect("Reserved key not found");
+1 -1
crates/tranquil-pds/tests/store_parity.rs
··· 1359 1359 let f = ParityFixture::new().await; 1360 1360 let did = test_did("sigkey"); 1361 1361 let expires = chrono::Utc::now() + chrono::Duration::hours(1); 1362 - let pub_key = format!("did:key:z6Mk{}", Uuid::new_v4().simple()); 1362 + let pub_key = Did::from(format!("did:key:z6Mk{}", Uuid::new_v4().simple())); 1363 1363 let priv_bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; 1364 1364 1365 1365 f.pg.infra
+4 -3
crates/tranquil-scopes/src/permission_set.rs
··· 220 220 Ok(record.value) 221 221 } 222 222 223 - async fn resolve_lexicon_did_authority(authority: &str) -> Result<String, ScopeExpansionError> { 223 + async fn resolve_lexicon_did_authority(authority: &str) -> Result<Did, ScopeExpansionError> { 224 224 let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or_else(|e| { 225 225 tracing::warn!("falling back to default DNS resolvers: {}", e); 226 226 TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()) ··· 239 239 .flat_map(|record| record.iter()) 240 240 .find_map(|data| { 241 241 let txt = String::from_utf8_lossy(data); 242 - txt.strip_prefix("did=").map(|did| did.to_string()) 242 + txt.strip_prefix("did=") 243 + .and_then(|did| Did::new(did.trim()).ok()) 243 244 }) 244 245 .ok_or_else(|| { 245 246 ScopeExpansionError::DnsResolution(format!( ··· 249 250 }) 250 251 } 251 252 252 - async fn resolve_did_to_pds(did: &str) -> Result<String, ScopeExpansionError> { 253 + async fn resolve_did_to_pds(did: &Did) -> Result<String, ScopeExpansionError> { 253 254 let client = Client::builder() 254 255 .timeout(std::time::Duration::from_secs(10)) 255 256 .build()
+22 -22
crates/tranquil-scopes/src/permissions.rs
··· 352 352 assert!(!perms.has_full_access()); 353 353 assert!(!perms.allows_repo(RepoAction::Create, "app.bsky.feed.post")); 354 354 assert!(!perms.allows_blob("image/png")); 355 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 355 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 356 356 assert!(!perms.allows_account(AccountAttr::Email, AccountAction::Manage)); 357 357 } 358 358 ··· 366 366 #[test] 367 367 fn test_transition_generic_without_chat_blocks_chat() { 368 368 let perms = ScopePermissions::from_scope_string(Some("transition:generic")); 369 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 370 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.listConvos")); 371 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.getMessages")); 369 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 370 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.listConvos"))); 371 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.getMessages"))); 372 372 } 373 373 374 374 #[test] 375 375 fn test_transition_generic_with_chat_allows_chat() { 376 376 let perms = 377 377 ScopePermissions::from_scope_string(Some("transition:generic transition:chat.bsky")); 378 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 379 - assert!(perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.listConvos")); 380 - assert!(perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.getMessages")); 378 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 379 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.listConvos"))); 380 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.getMessages"))); 381 381 } 382 382 383 383 #[test] 384 384 fn test_transition_chat_only_allows_chat() { 385 385 let perms = ScopePermissions::from_scope_string(Some("transition:chat.bsky")); 386 - assert!(!perms.allows_repo(RepoAction::Create, "app.bsky.feed.post")); 387 - assert!(perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.getMessages")); 388 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 386 + assert!(!perms.allows_repo(RepoAction::Create, &c("app.bsky.feed.post"))); 387 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.getMessages"))); 388 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 389 389 } 390 390 391 391 #[test] ··· 448 448 let perms = ScopePermissions::from_scope_string(Some( 449 449 "rpc:app.bsky.feed.getTimeline?aud=did:web:api.bsky.app", 450 450 )); 451 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 452 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed")); 453 - assert!(!perms.allows_rpc("did:web:other.service", "app.bsky.feed.getTimeline")); 451 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 452 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed"))); 453 + assert!(!perms.allows_rpc("did:web:other.service", &c("app.bsky.feed.getTimeline"))); 454 454 } 455 455 456 456 #[test] 457 457 fn test_granular_rpc_wildcard_aud() { 458 458 let perms = 459 459 ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.getTimeline?aud=*")); 460 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 461 - assert!(perms.allows_rpc("did:web:any.service", "app.bsky.feed.getTimeline")); 462 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed")); 460 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 461 + assert!(perms.allows_rpc("did:web:any.service", &c("app.bsky.feed.getTimeline"))); 462 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed"))); 463 463 } 464 464 465 465 #[test] ··· 539 539 assert!(!perms.allows_repo(RepoAction::Delete, &c("cafe.oyster.record"))); 540 540 assert!(!perms.allows_repo(RepoAction::Update, &c("cafe.oyster.record"))); 541 541 assert!(perms.allows_blob("image/png")); 542 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 542 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 543 543 } 544 544 545 545 #[test] ··· 550 550 assert!(!perms.allows_repo(RepoAction::Delete, &c("cafe.oyster.record"))); 551 551 assert!(!perms.allows_repo(RepoAction::Update, &c("cafe.oyster.record"))); 552 552 assert!(!perms.allows_blob("image/png")); 553 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 553 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 554 554 } 555 555 556 556 #[test] ··· 558 558 let perms = ScopePermissions::from_scope_string(Some( 559 559 "rpc:app.bsky.feed.getAuthorFeed?aud=did:web:api.bsky.app#bsky_appview", 560 560 )); 561 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed")); 561 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed"))); 562 562 assert!(perms.allows_rpc( 563 563 "did:web:api.bsky.app#bsky_appview", 564 564 "app.bsky.feed.getAuthorFeed" ··· 567 567 "did:web:api.bsky.app#other_service", 568 568 "app.bsky.feed.getAuthorFeed" 569 569 )); 570 - assert!(!perms.allows_rpc("did:web:other.app", "app.bsky.feed.getAuthorFeed")); 571 - assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline")); 570 + assert!(!perms.allows_rpc("did:web:other.app", &c("app.bsky.feed.getAuthorFeed"))); 571 + assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline"))); 572 572 } 573 573 574 574 #[test] ··· 576 576 let perms = ScopePermissions::from_scope_string(Some( 577 577 "rpc:app.bsky.feed.getAuthorFeed?aud=did:web:api.bsky.app", 578 578 )); 579 - assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed")); 579 + assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed"))); 580 580 assert!(perms.allows_rpc( 581 581 "did:web:api.bsky.app#bsky_appview", 582 582 "app.bsky.feed.getAuthorFeed"
+3 -3
crates/tranquil-store/benches/metastore.rs
··· 507 507 futures::stream::iter(user_ids.iter().zip(dids.iter())) 508 508 .fold((), |(), (uid, did)| async { 509 509 pg_create_user(pg, *uid, did).await; 510 - let did_typed = Did::from(did.clone()); 510 + let did = Did::from(did.clone()); 511 511 let handle = Handle::from(format!("bench.{}.invalid", uid.as_simple())); 512 512 repo.create_repo( 513 513 *uid, ··· 612 612 let collection = Nsid::from("app.bsky.feed.post".to_string()); 613 613 pg_create_user(pg, user_id, &did).await; 614 614 let repo = tranquil_db::postgres::PostgresRepoRepository::new(pg.clone()); 615 - let did_typed = Did::from(did.clone()); 615 + let did = Did::from(did); 616 616 let handle = Handle::from(format!("bench.{}.invalid", user_id.as_simple())); 617 617 repo.create_repo( 618 618 user_id, ··· 668 668 let collection = Nsid::from("app.bsky.feed.post".to_string()); 669 669 pg_create_user(pg, user_id, &did).await; 670 670 let repo = tranquil_db::postgres::PostgresRepoRepository::new(pg.clone()); 671 - let did_typed = Did::from(did.clone()); 671 + let did = Did::from(did); 672 672 let handle = Handle::from(format!("bench.{}.invalid", user_id.as_simple())); 673 673 repo.create_repo( 674 674 user_id,
+12 -5
crates/tranquil-store/src/metastore/client.rs
··· 1726 1726 recv(rx).await 1727 1727 } 1728 1728 1729 - async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError> { 1729 + async fn get_app_password_hashes_by_did( 1730 + &self, 1731 + did: &Did, 1732 + ) -> Result<Vec<PasswordHash>, DbError> { 1730 1733 let (tx, rx) = oneshot::channel(); 1731 1734 self.pool.send(MetastoreRequest::Session( 1732 1735 SessionRequest::GetAppPasswordHashesByDid { ··· 2041 2044 async fn reserve_signing_key( 2042 2045 &self, 2043 2046 did: Option<&Did>, 2044 - public_key_did_key: &str, 2047 + public_key_did_key: &Did, 2045 2048 private_key_bytes: &[u8], 2046 2049 expires_at: DateTime<Utc>, 2047 2050 ) -> Result<Uuid, DbError> { ··· 2059 2062 2060 2063 async fn get_reserved_signing_key( 2061 2064 &self, 2062 - public_key_did_key: &str, 2065 + public_key_did_key: &Did, 2063 2066 ) -> Result<Option<ReservedSigningKey>, DbError> { 2064 2067 let (tx, rx) = oneshot::channel(); 2065 2068 self.pool.send(MetastoreRequest::Infra( ··· 2466 2469 2467 2470 async fn get_reserved_signing_key_full( 2468 2471 &self, 2469 - public_key_did_key: &str, 2472 + public_key_did_key: &Did, 2470 2473 ) -> Result<Option<ReservedSigningKeyFull>, DbError> { 2471 2474 let (tx, rx) = oneshot::channel(); 2472 2475 self.pool.send(MetastoreRequest::Infra( ··· 3730 3733 recv(rx).await 3731 3734 } 3732 3735 3733 - async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError> { 3736 + async fn admin_update_password( 3737 + &self, 3738 + did: &Did, 3739 + password_hash: &PasswordHash, 3740 + ) -> Result<u64, DbError> { 3734 3741 let (tx, rx) = oneshot::channel(); 3735 3742 self.pool 3736 3743 .send(MetastoreRequest::User(UserRequest::AdminUpdatePassword {
+14 -16
crates/tranquil-store/src/metastore/handler.rs
··· 253 253 impl RepoRequest { 254 254 fn routing(&self, user_hashes: &UserHashMap) -> Routing { 255 255 match self { 256 - Self::CreateRepoFull { did, .. } => did_to_routing(did.as_str()), 256 + Self::CreateRepoFull { did, .. } => did_to_routing(did), 257 257 Self::UpdateRepoRoot { user_id, .. } 258 258 | Self::UpdateRepoRev { user_id, .. } 259 259 | Self::DeleteRepo { user_id, .. } ··· 262 262 | Self::GetRepoRootCidByUserId { user_id, .. } => uuid_to_routing(user_hashes, user_id), 263 263 Self::GetRepoRootByDid { did, .. } 264 264 | Self::GetAccountWithRepo { did, .. } 265 - | Self::UpdateRepoStatus { did, .. } => did_to_routing(did.as_str()), 265 + | Self::UpdateRepoStatus { did, .. } => did_to_routing(did), 266 266 Self::CountRepos { .. } 267 267 | Self::GetReposWithoutRev { .. } 268 268 | Self::ListReposPaginated { .. } => Routing::Global, ··· 503 503 impl CommitRequest { 504 504 fn routing(&self, user_hashes: &UserHashMap) -> Routing { 505 505 match self { 506 - Self::ApplyCommit { input, .. } => did_to_routing(input.did.as_str()), 506 + Self::ApplyCommit { input, .. } => did_to_routing(&input.did), 507 507 Self::ImportRepoData { user_id, .. } 508 508 | Self::InsertRecordBlobs { 509 509 repo_id: user_id, .. ··· 644 644 Self::ListMissingBlobs { repo_id, .. } 645 645 | Self::CountDistinctRecordBlobs { repo_id, .. } 646 646 | Self::GetBlobsForExport { repo_id, .. } => uuid_to_routing(user_hashes, repo_id), 647 - Self::ListBlobsSinceRev { did, .. } => did_to_routing(did.as_str()), 647 + Self::ListBlobsSinceRev { did, .. } => did_to_routing(did), 648 648 } 649 649 } 650 650 } ··· 731 731 } 732 732 | Self::CountAuditLogEntries { 733 733 delegated_did: did, .. 734 - } => did_to_routing(did.as_str()), 734 + } => did_to_routing(did), 735 735 Self::CreateDelegation { delegated_did, .. } 736 736 | Self::RevokeDelegation { delegated_did, .. } 737 737 | Self::UpdateDelegationScopes { delegated_did, .. } ··· 743 743 | Self::ControlsAnyAccounts { 744 744 did: controller_did, 745 745 .. 746 - } => did_to_routing(controller_did.as_str()), 746 + } => did_to_routing(controller_did), 747 747 } 748 748 } 749 749 } ··· 822 822 match self { 823 823 Self::CreateExternalIdentity { did, .. } 824 824 | Self::GetExternalIdentitiesByDid { did, .. } 825 - | Self::DeleteExternalIdentity { did, .. } => did_to_routing(did.as_str()), 825 + | Self::DeleteExternalIdentity { did, .. } => did_to_routing(did), 826 826 Self::GetExternalIdentityByProvider { .. } 827 827 | Self::UpdateExternalIdentityLogin { .. } 828 828 | Self::ConsumeSsoAuthState { .. } ··· 964 964 | Self::UpdateLastReauth { did, .. } 965 965 | Self::GetSessionMfaStatus { did, .. } 966 966 | Self::UpdateMfaVerified { did, .. } 967 - | Self::GetAppPasswordHashesByDid { did, .. } => did_to_routing(did.as_str()), 967 + | Self::GetAppPasswordHashesByDid { did, .. } => did_to_routing(did), 968 968 Self::RefreshSessionAtomic { data, .. } => did_to_routing(data.did.as_str()), 969 969 Self::ListAppPasswords { user_id, .. } 970 970 | Self::GetAppPasswordsForLogin { user_id, .. } ··· 1721 1721 } 1722 1722 | Self::SetRecoveryToken { did, .. } 1723 1723 | Self::EnableTotpVerified { did, .. } 1724 - | Self::SetTwoFactorEnabled { did, .. } => did_to_routing(did.as_str()), 1724 + | Self::SetTwoFactorEnabled { did, .. } => did_to_routing(did), 1725 1725 1726 1726 Self::GetCommsPrefs { user_id, .. } 1727 1727 | Self::GetUserKeyById { user_id, .. } ··· 2101 2101 | Self::GetAdminAccountInfoByDid { did, .. } 2102 2102 | Self::GetDeletionRequestByDid { did, .. } 2103 2103 | Self::GetPlcTokensByDid { did, .. } 2104 - | Self::CountPlcTokensByDid { did, .. } => did_to_routing(did.as_str()), 2104 + | Self::CountPlcTokensByDid { did, .. } => did_to_routing(did), 2105 2105 Self::GetBlobStorageKeyByCid { cid, .. } | Self::DeleteBlobByCid { cid, .. } => { 2106 2106 cid_to_routing(cid) 2107 2107 } ··· 2405 2405 | Self::ListSessionsByDid { did, .. } 2406 2406 | Self::DeleteSessionsByDid { did, .. } 2407 2407 | Self::DeleteSessionsByDidExcept { did, .. } 2408 - | Self::DeleteSessionById { did, .. } => did_to_routing(did.as_str()), 2409 - Self::RevokeTokensForController { delegated_did, .. } => { 2410 - did_to_routing(delegated_did.as_str()) 2411 - } 2408 + | Self::DeleteSessionById { did, .. } => did_to_routing(did), 2409 + Self::RevokeTokensForController { delegated_did, .. } => did_to_routing(delegated_did), 2412 2410 Self::SetAuthorizationDid { did, .. } 2413 2411 | Self::UpdateAuthorizationRequest { did, .. } 2414 2412 | Self::MarkRequestAuthenticated { did, .. } 2415 - | Self::SetRequestDid { did, .. } => did_to_routing(did.as_str()), 2413 + | Self::SetRequestDid { did, .. } => did_to_routing(did), 2416 2414 Self::CreateToken { .. } 2417 2415 | Self::GetTokenById { .. } 2418 2416 | Self::GetTokenByRefreshToken { .. } ··· 6284 6282 .unwrap(); 6285 6283 let user_hashes = ms.user_hashes().as_ref(); 6286 6284 let did = Did::from("did:plc:limpet".to_string()); 6287 - let expected = did_to_routing(did.as_str()); 6285 + let expected = did_to_routing(&did); 6288 6286 let sid = SessionId::new(7); 6289 6287 6290 6288 let (tx, _rx) = oneshot::channel();
+1 -1
crates/tranquil-store/src/metastore/infra_ops.rs
··· 1461 1461 Ok(val.map(|v| ReservedSigningKeyFull { 1462 1462 id: v.id, 1463 1463 did: v.did.and_then(|d| Did::new(d).ok()), 1464 - public_key_did_key: v.public_key_did_key, 1464 + public_key_did_key: Did::from(v.public_key_did_key), 1465 1465 private_key_bytes: v.private_key_bytes, 1466 1466 expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), 1467 1467 used_at: v.used_at_ms.and_then(DateTime::from_timestamp_millis),