db: Did type for repos and handlers

Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
Lewis
2026-07-12 08:03:14 +02:00
parent 6ca6c45605
commit eb1a89dc58
58 changed files with 421 additions and 444 deletions
@@ -200,7 +200,7 @@ pub async fn get_account_infos(
return Err(ApiError::InvalidRequest("dids is required".into()));
}
let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect();
let dids: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect();
let accounts = state
.repos
.infra
+4 -4
View File
@@ -15,7 +15,7 @@ use tranquil_pds::state::AppState;
#[serde(rename_all = "camelCase")]
pub struct DisableInviteCodesInput {
pub codes: Option<Vec<String>>,
pub accounts: Option<Vec<String>>,
pub accounts: Option<Vec<Did>>,
}
pub async fn disable_invite_codes(
@@ -97,7 +97,7 @@ pub async fn get_invite_codes(
let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect();
let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
let creator_dids: std::collections::HashMap<uuid::Uuid, tranquil_types::Did> = state
let creator_dids: std::collections::HashMap<uuid::Uuid, Did> = state
.repos
.infra
.get_user_dids_by_ids(&user_ids)
@@ -167,7 +167,7 @@ pub async fn disable_account_invites(
if account.is_empty() {
return Err(ApiError::InvalidRequest("account is required".into()));
}
let account_did: tranquil_types::Did = account
let account_did: Did = account
.parse()
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
@@ -200,7 +200,7 @@ pub async fn enable_account_invites(
if account.is_empty() {
return Err(ApiError::InvalidRequest("account is required".into()));
}
let account_did: tranquil_types::Did = account
let account_did: Did = account
.parse()
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
+2 -2
View File
@@ -195,12 +195,12 @@ pub fn extract_verification_recipient(
}
}
pub fn create_self_hosted_did_web(handle: &str) -> Result<String, ApiError> {
pub fn create_self_hosted_did_web(handle: &str) -> Result<Did, ApiError> {
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
return Err(ApiError::SelfHostedDidWebDisabled);
}
let encoded_handle = handle.replace(':', "%3A");
Ok(format!("did:web:{}", encoded_handle))
Ok(Did::from(format!("did:web:{}", encoded_handle)))
}
pub enum CredentialMatch {
+2 -2
View File
@@ -38,7 +38,7 @@ pub async fn list_controllers(
async move {
if c.handle.is_none() {
c.handle = did_resolver
.fetch_did_document(c.did.as_str())
.fetch_did_document(&c.did)
.await
.ok()
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc));
@@ -77,7 +77,7 @@ pub async fn add_controller(
}
match state
.cross_pds_oauth
.check_remote_is_delegated(pds_url, input.controller_did.as_str())
.check_remote_is_delegated(pds_url, &input.controller_did)
.await
{
Some(true) => {
+8 -8
View File
@@ -25,7 +25,7 @@ pub struct CreateAccountInput {
pub invite_code: Option<String>,
pub did: Option<String>,
pub did_type: Option<String>,
pub signing_key: Option<String>,
pub signing_key: Option<Did>,
pub verification_channel: Option<tranquil_db_traits::CommsChannel>,
pub discord_username: Option<String>,
pub telegram_username: Option<String>,
@@ -47,7 +47,7 @@ pub struct CreateAccountOutput {
async fn try_reactivate_migration(
state: &AppState,
did: &str,
did: &Did,
handle: &Handle,
email: &Option<String>,
verification_channel: tranquil_db_traits::CommsChannel,
@@ -112,7 +112,7 @@ async fn try_reactivate_migration(
}
};
let session_data = tranquil_db_traits::SessionTokenCreate {
did: did_typed.clone(),
did: did.clone(),
access_jti: access_meta.jti.clone(),
refresh_jti: refresh_meta.jti.clone(),
access_expires_at: access_meta.expires_at,
@@ -132,7 +132,7 @@ async fn try_reactivate_migration(
super::provision::enqueue_migration_verification(
state,
reactivated.user_id,
&did_typed,
did,
verification_channel,
recipient,
)
@@ -309,7 +309,7 @@ pub async fn create_account(
let signing_key = key_result.signing_key;
let reserved_key_id = key_result.reserved_key_id;
let did_type = input.did_type.as_deref().unwrap_or("plc");
let did = match did_type {
let did: Did = match did_type {
"web" => {
let self_hosted_did = match common::create_self_hosted_did_web(&handle) {
Ok(d) => d,
@@ -456,7 +456,7 @@ pub async fn create_account(
let create_input = tranquil_db_traits::CreatePasswordAccountInput {
handle: handle.clone(),
email: email.clone(),
did: did_for_commit.clone(),
did: did.clone(),
password_hash,
preferred_comms_channel,
discord_username: comms.discord,
@@ -508,7 +508,7 @@ pub async fn create_account(
super::provision::enqueue_signup_verification(
&state,
user_id,
&did_for_commit,
&did,
verification_channel,
recipient,
)
@@ -518,7 +518,7 @@ pub async fn create_account(
super::provision::enqueue_migration_verification(
&state,
user_id,
&did_for_commit,
&did,
verification_channel,
recipient,
)
+3 -7
View File
@@ -44,10 +44,6 @@ pub async fn resolve_handle(
if handle_str.is_empty() {
return ApiError::InvalidRequest("handle is required".into()).into_response();
}
let cache_key = tranquil_pds::cache_keys::handle_key(handle_str);
if let Some(did) = state.cache.get(&cache_key).await {
return DidResponse::response(did).into_response();
}
let handle: Handle = match handle_str.parse() {
Ok(h) => h,
Err(_) => {
@@ -163,8 +159,8 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
let encoded_handle = handle.replace(':', "%3A");
let expected_did = format!("did:web:{}", encoded_handle);
let expected_did_typed: tranquil_pds::types::Did = match expected_did.parse() {
let expected_did: tranquil_pds::types::Did = match format!("did:web:{}", encoded_handle).parse()
{
Ok(d) => d,
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
};
@@ -350,7 +346,7 @@ pub async fn verify_did_web(
did: &str,
hostname: &str,
handle: &str,
expected_signing_key: Option<&str>,
expected_signing_key: Option<&Did>,
) -> Result<(), DidWebVerifyError> {
let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname);
let subdomain_host = format!("{}.{}", handle, hostname_for_handles);
@@ -38,7 +38,7 @@ pub async fn submit_plc_genesis(
state: &AppState,
signing_key: &SigningKey,
handle: &Handle,
) -> Result<String, ApiError> {
) -> Result<Did, ApiError> {
let hostname = &tranquil_config::get().server.hostname;
let pds_endpoint = format!("https://{}", hostname);
@@ -122,7 +122,7 @@ pub struct SigningKeyResult {
pub async fn resolve_signing_key(
state: &AppState,
signing_key_did: Option<&str>,
signing_key_did: Option<&Did>,
) -> Result<SigningKeyResult, ApiError> {
match signing_key_did {
Some(key_did) => {
+14 -5
View File
@@ -66,17 +66,26 @@ pub struct CreateReportOutput {
struct ReportServiceConfig {
url: String,
did: String,
did: Did,
}
fn get_report_service_config() -> Option<ReportServiceConfig> {
let cfg = tranquil_config::get();
let url = cfg.moderation.report_service_url.clone()?;
let did = cfg.moderation.report_service_did.clone()?;
if url.is_empty() || did.is_empty() {
let did_str = cfg.moderation.report_service_did.as_deref()?;
if url.is_empty() || did_str.is_empty() {
return None;
}
Some(ReportServiceConfig { url, did })
match did_str.parse::<Did>() {
Ok(did) => Some(ReportServiceConfig { url, did }),
Err(_) => {
warn!(
report_service_did = did_str,
"invalid report_service_did, handling reports locally"
);
None
}
}
}
pub async fn create_report(
@@ -97,7 +106,7 @@ async fn proxy_to_report_service(
state: &AppState,
auth_user: &tranquil_pds::auth::AuthenticatedUser,
service_url: &str,
service_did: &str,
service_did: &Did,
input: &CreateReportInput,
) -> Response {
if let Err(e) = is_ssrf_safe(service_url) {
+6 -11
View File
@@ -221,17 +221,12 @@ pub async fn import_repo(
})?;
let new_rev = Tid::now(LimitedU32::MIN);
let new_rev_str = new_rev.to_string();
let (commit_bytes, _sig) = create_signed_commit(
did,
import_result.data_cid,
&new_rev_str,
None,
&signing_key,
)
.map_err(|e| {
error!("Failed to create new commit: {}", e);
ApiError::InternalError(None)
})?;
let (commit_bytes, _sig) =
create_signed_commit(did, import_result.data_cid, &new_rev, None, &signing_key)
.map_err(|e| {
error!("Failed to create new commit: {}", e);
ApiError::InternalError(None)
})?;
let new_root_cid: cid::Cid =
state.block_store.put(&commit_bytes).await.map_err(|e| {
error!("Failed to store new commit block: {:?}", e);
@@ -133,7 +133,7 @@ async fn assert_valid_did_document_for_service(
if did.as_str().starts_with("did:plc:") {
let max_attempts = if with_retry { 5 } else { 1 };
let cache_for_retry = cache.clone();
let did_owned = did.as_str().to_string();
let did_owned = did.clone();
let expected_owned = expected_endpoint.clone();
let attempt_counter = Arc::new(AtomicUsize::new(0));
@@ -387,7 +387,7 @@ pub async fn activate_account(
.cache
.delete(&tranquil_pds::cache_keys::plc_data_key(&did))
.await;
if state.did_resolver.refresh_did(did.as_str()).await.is_err() {
if state.did_resolver.refresh_did(&did).await.is_err() {
warn!(
"[MIGRATION] activateAccount: Failed to refresh DID cache for {}",
did
+5 -4
View File
@@ -19,6 +19,7 @@ use tranquil_pds::auth::{Auth, NotTakendown};
use tranquil_pds::oauth::scopes::{AccountAction, AccountAttr};
use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{AtIdentifier, Did};
const EMAIL_UPDATE_TTL: Duration = Duration::from_secs(30 * 60);
@@ -37,7 +38,7 @@ struct PendingEmailUpdate {
async fn get_pending_email_update(
cache: &dyn tranquil_pds::cache::Cache,
did: &str,
did: &Did,
) -> Option<PendingEmailUpdate> {
cache
.get(&tranquil_pds::cache_keys::email_update_key(did))
@@ -79,7 +80,7 @@ pub async fn request_email_update(
if token_required {
let token = tranquil_pds::auth::email_token::create_email_token(
state.cache.as_ref(),
auth.did.as_str(),
&auth.did,
tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail,
)
.await
@@ -249,7 +250,7 @@ pub async fn update_email(
tranquil_pds::auth::email_token::validate_email_token(
state.cache.as_ref(),
did.as_str(),
did,
tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail,
token,
)
@@ -298,7 +299,7 @@ pub async fn update_email(
let short_token_result = tranquil_pds::auth::email_token::validate_email_token(
state.cache.as_ref(),
did.as_str(),
did,
tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail,
token,
)
+2 -2
View File
@@ -72,8 +72,8 @@ pub struct CreateInviteCodesOutput {
#[derive(Serialize)]
pub struct AccountCodes {
pub account: String,
pub codes: Vec<String>,
pub account: Did,
pub codes: Vec<InviteCodeValue>,
}
pub async fn create_invite_codes(
@@ -39,7 +39,7 @@ pub struct CreatePasskeyAccountInput {
pub invite_code: Option<String>,
pub did: Option<String>,
pub did_type: Option<String>,
pub signing_key: Option<String>,
pub signing_key: Option<Did>,
pub verification_channel: Option<tranquil_db_traits::CommsChannel>,
pub discord_username: Option<String>,
pub telegram_username: Option<String>,
@@ -154,7 +154,7 @@ pub async fn create_passkey_account(
let secret_key = key_result.signing_key;
let reserved_key_id = key_result.reserved_key_id;
let did = match did_type {
let did: Did = match did_type {
"web" => {
let self_hosted_did = match common::create_self_hosted_did_web(&handle) {
Ok(d) => d,
@@ -269,13 +269,9 @@ pub async fn create_passkey_account(
None
};
let did_typed: Did = match did.parse() {
Ok(d) => d,
Err(_) => return Err(ApiError::InternalError(Some("Invalid DID".into()))),
};
let repo = match crate::identity::provision::init_genesis_repo(
&state,
&did_typed,
&did,
&secret_key,
&secret_key_bytes,
)
@@ -303,7 +299,7 @@ pub async fn create_passkey_account(
let create_input = tranquil_db_traits::CreatePasskeyAccountInput {
handle: handle.clone(),
email: email.clone().unwrap_or_default(),
did: did_typed.clone(),
did: did.clone(),
preferred_comms_channel: verification_channel,
discord_username: comms.discord,
telegram_username: comms.telegram,
@@ -353,7 +349,7 @@ pub async fn create_passkey_account(
crate::identity::provision::enqueue_signup_verification(
&state,
user_id,
&did_typed,
&did,
verification_channel,
&verification_recipient,
)
@@ -367,7 +363,7 @@ pub async fn create_passkey_account(
let refresh_jti = Jti::from(uuid::Uuid::new_v4().to_string());
let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24);
let session_data = tranquil_db_traits::SessionTokenCreate {
did: did_typed.clone(),
did: did.clone(),
access_jti: token_meta.jti.clone(),
refresh_jti,
access_expires_at: token_meta.expires_at,
@@ -688,7 +684,7 @@ pub async fn request_passkey_recovery(
if let Err(e) = state
.repos
.user
.set_recovery_token(&user.did, &recovery_token_hash, expires_at)
.set_recovery_token(&user.did, recovery_token_hash.as_str(), expires_at)
.await
{
error!("Error updating recovery token: {:?}", e);
+4 -5
View File
@@ -575,7 +575,7 @@ pub async fn refresh_session(
&session_row.did,
&key_bytes,
session_row.scope.as_deref(),
session_row.controller_did.as_deref(),
session_row.controller_did.as_ref(),
None,
) {
Ok(m) => m,
@@ -747,7 +747,7 @@ fn remint_grace_tokens(
&replay.did,
key_bytes,
replay.scope.as_deref(),
replay.controller_did.as_deref(),
replay.controller_did.as_ref(),
None,
&replay.access_jti,
replay.access_expires_at,
@@ -915,7 +915,6 @@ pub async fn confirm_signup(
let session = match crate::identity::provision::create_and_store_session(
&state,
&row.did,
&row.did,
&key_bytes,
"transition:generic transition:chat.bsky",
None,
@@ -957,7 +956,7 @@ pub struct AutoResendResult {
}
pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option<AutoResendResult> {
let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did.as_str());
let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did);
let debounced = state.cache.get(&debounce_key).await.is_some();
let row = match state.repos.user.get_resend_verification_by_did(did).await {
Ok(Some(row)) => row,
@@ -1224,7 +1223,7 @@ pub async fn revoke_all_sessions(
state
.repos
.oauth
.delete_sessions_by_did_except(&auth.did, &jti_typed)
.delete_sessions_by_did_except(&auth.did, &token_id)
.await
.log_db_err("revoking OAuth sessions")?;
} else {
@@ -10,17 +10,18 @@ use serde::{Deserialize, Serialize};
use tracing::{error, info};
use tranquil_pds::api::error::ApiError;
use tranquil_pds::state::AppState;
use tranquil_pds::types::Did;
const SECP256K1_MULTICODEC_PREFIX: [u8; 2] = [0xe7, 0x01];
fn public_key_to_did_key(signing_key: &SigningKey) -> String {
fn public_key_to_did_key(signing_key: &SigningKey) -> Did {
let verifying_key = signing_key.verifying_key();
let compressed_pubkey = verifying_key.to_sec1_bytes();
let mut multicodec_key = Vec::with_capacity(2 + compressed_pubkey.len());
multicodec_key.extend_from_slice(&SECP256K1_MULTICODEC_PREFIX);
multicodec_key.extend_from_slice(&compressed_pubkey);
let encoded = multibase::encode(multibase::Base::Base58Btc, &multicodec_key);
format!("did:key:{}", encoded)
Did::from(format!("did:key:{}", encoded))
}
#[derive(Deserialize)]
@@ -31,7 +32,7 @@ pub struct ReserveSigningKeyInput {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReserveSigningKeyOutput {
pub signing_key: String,
pub signing_key: Did,
}
pub async fn reserve_signing_key(
+34 -34
View File
@@ -12,20 +12,20 @@ use tranquil_types::{Did, Jti, Nsid};
type HmacSha256 = Hmac<Sha256>;
pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> {
pub fn create_access_token(did: &Did, key_bytes: &[u8]) -> Result<String> {
Ok(create_access_token_with_metadata(did, key_bytes)?.token)
}
pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> {
pub fn create_refresh_token(did: &Did, key_bytes: &[u8]) -> Result<String> {
Ok(create_refresh_token_with_metadata(did, key_bytes)?.token)
}
pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
pub fn create_access_token_with_metadata(did: &Did, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
create_access_token_with_scope_metadata(did, key_bytes, None, None)
}
pub fn create_access_token_with_scope_metadata(
did: &str,
did: &Did,
key_bytes: &[u8],
scopes: Option<&str>,
hostname: Option<&str>,
@@ -42,14 +42,14 @@ pub fn create_access_token_with_scope_metadata(
}
pub fn create_access_token_with_delegation(
did: &str,
did: &Did,
key_bytes: &[u8],
scopes: Option<&str>,
controller_did: Option<&str>,
controller_did: Option<&Did>,
hostname: Option<&str>,
) -> Result<TokenWithMetadata> {
let scope = scopes.unwrap_or(TokenScope::Access.as_str());
let act = controller_did.map(|c| ActClaim { sub: c.to_string() });
let act = controller_did.map(|c| ActClaim { sub: c.clone() });
create_signed_token_with_act(
did,
scope,
@@ -62,7 +62,7 @@ pub fn create_access_token_with_delegation(
}
pub fn create_refresh_token_with_metadata(
did: &str,
did: &Did,
key_bytes: &[u8],
) -> Result<TokenWithMetadata> {
create_signed_token_with_metadata(
@@ -79,16 +79,16 @@ pub fn create_refresh_token_with_metadata(
/// refresh grace window to reproduce a session's current access token without
/// persisting the signed JWT itself.
pub fn create_access_token_with_jti(
did: &str,
did: &Did,
key_bytes: &[u8],
scopes: Option<&str>,
controller_did: Option<&str>,
controller_did: Option<&Did>,
hostname: Option<&str>,
jti: &Jti,
expires_at: DateTime<Utc>,
) -> Result<String> {
let scope = scopes.unwrap_or(TokenScope::Access.as_str());
let act = controller_did.map(|c| ActClaim { sub: c.to_string() });
let act = controller_did.map(|c| ActClaim { sub: c.clone() });
Ok(create_signed_token_pinned(
did,
scope,
@@ -105,7 +105,7 @@ pub fn create_access_token_with_jti(
/// Re-mint a refresh token carrying a specific `jti` and expiry. Counterpart to
/// [`create_access_token_with_jti`] for the refresh grace window.
pub fn create_refresh_token_with_jti(
did: &str,
did: &Did,
key_bytes: &[u8],
jti: &Jti,
expires_at: DateTime<Utc>,
@@ -124,8 +124,8 @@ pub fn create_refresh_token_with_jti(
}
pub fn create_service_token(
did: &str,
aud: &str,
did: &Did,
aud: &Did,
lxm: Option<&Nsid>,
key_bytes: &[u8],
) -> Result<String> {
@@ -137,9 +137,9 @@ pub fn create_service_token(
.timestamp();
let claims = Claims {
iss: did.to_owned(),
sub: did.to_owned(),
aud: aud.to_owned(),
iss: did.clone(),
sub: did.clone(),
aud: aud.to_string(),
exp: expiration,
iat: Utc::now().timestamp(),
scope: None,
@@ -152,7 +152,7 @@ pub fn create_service_token(
}
fn create_signed_token_with_metadata(
did: &str,
did: &Did,
scope: &str,
typ: TokenType,
key_bytes: &[u8],
@@ -163,7 +163,7 @@ fn create_signed_token_with_metadata(
}
fn create_signed_token_with_act(
did: &str,
did: &Did,
scope: &str,
typ: TokenType,
key_bytes: &[u8],
@@ -180,7 +180,7 @@ fn create_signed_token_with_act(
#[allow(clippy::too_many_arguments)]
fn create_signed_token_pinned(
did: &str,
did: &Did,
scope: &str,
typ: TokenType,
key_bytes: &[u8],
@@ -200,8 +200,8 @@ fn create_signed_token_pinned(
});
let claims = Claims {
iss: did.to_owned(),
sub: did.to_owned(),
iss: did.clone(),
sub: did.clone(),
aud: format!("did:web:{}", aud_hostname),
exp: expiration,
iat: Utc::now().timestamp(),
@@ -243,16 +243,16 @@ fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: TokenType) -> Re
Ok(format!("{}.{}", message, signature_b64))
}
pub fn create_access_token_hs256(did: &str, secret: &[u8]) -> Result<String> {
pub fn create_access_token_hs256(did: &Did, secret: &[u8]) -> Result<String> {
Ok(create_access_token_hs256_with_metadata(did, secret)?.token)
}
pub fn create_refresh_token_hs256(did: &str, secret: &[u8]) -> Result<String> {
pub fn create_refresh_token_hs256(did: &Did, secret: &[u8]) -> Result<String> {
Ok(create_refresh_token_hs256_with_metadata(did, secret)?.token)
}
pub fn create_access_token_hs256_with_metadata(
did: &str,
did: &Did,
secret: &[u8],
) -> Result<TokenWithMetadata> {
create_hs256_token_with_metadata(
@@ -265,7 +265,7 @@ pub fn create_access_token_hs256_with_metadata(
}
pub fn create_refresh_token_hs256_with_metadata(
did: &str,
did: &Did,
secret: &[u8],
) -> Result<TokenWithMetadata> {
create_hs256_token_with_metadata(
@@ -278,8 +278,8 @@ pub fn create_refresh_token_hs256_with_metadata(
}
pub fn create_service_token_hs256(
did: &str,
aud: &str,
did: &Did,
aud: &Did,
lxm: &Nsid,
secret: &[u8],
) -> Result<String> {
@@ -289,9 +289,9 @@ pub fn create_service_token_hs256(
.timestamp();
let claims = Claims {
iss: did.to_owned(),
sub: did.to_owned(),
aud: aud.to_owned(),
iss: did.clone(),
sub: did.clone(),
aud: aud.to_string(),
exp: expiration,
iat: Utc::now().timestamp(),
scope: None,
@@ -304,7 +304,7 @@ pub fn create_service_token_hs256(
}
fn create_hs256_token_with_metadata(
did: &str,
did: &Did,
scope: &str,
typ: TokenType,
secret: &[u8],
@@ -318,8 +318,8 @@ fn create_hs256_token_with_metadata(
let jti = Jti::new(uuid::Uuid::new_v4().to_string());
let claims = Claims {
iss: did.to_owned(),
sub: did.to_owned(),
iss: did.clone(),
sub: did.clone(),
aud: format!(
"did:web:{}",
tranquil_config::try_get()
+5 -5
View File
@@ -204,13 +204,13 @@ impl std::error::Error for TokenDecodeError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActClaim {
pub sub: String,
pub sub: Did,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub iss: String,
pub sub: String,
pub iss: Did,
pub sub: Did,
pub aud: String,
pub exp: i64,
pub iat: i64,
@@ -231,8 +231,8 @@ pub struct Header {
#[derive(Debug, Serialize, Deserialize)]
pub struct UnsafeClaims {
pub iss: String,
pub sub: Option<String>,
pub iss: Did,
pub sub: Option<Did>,
}
pub struct TokenData<T> {
+1 -1
View File
@@ -14,7 +14,7 @@ use tranquil_types::{Did, Jti};
type HmacSha256 = Hmac<Sha256>;
pub fn get_did_from_token(token: &str) -> Result<String, TokenDecodeError> {
pub fn get_did_from_token(token: &str) -> Result<Did, TokenDecodeError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(TokenDecodeError::InvalidFormat);
+4 -4
View File
@@ -189,7 +189,7 @@ pub struct ReservedSigningKey {
pub struct ReservedSigningKeyFull {
pub id: Uuid,
pub did: Option<Did>,
pub public_key_did_key: String,
pub public_key_did_key: Did,
pub private_key_bytes: Vec<u8>,
pub expires_at: DateTime<Utc>,
pub used_at: Option<DateTime<Utc>>,
@@ -314,14 +314,14 @@ pub trait InfraRepository: Send + Sync {
async fn reserve_signing_key(
&self,
did: Option<&Did>,
public_key_did_key: &str,
public_key_did_key: &Did,
private_key_bytes: &[u8],
expires_at: DateTime<Utc>,
) -> Result<Uuid, DbError>;
async fn get_reserved_signing_key(
&self,
public_key_did_key: &str,
public_key_did_key: &Did,
) -> Result<Option<ReservedSigningKey>, DbError>;
async fn mark_signing_key_used(&self, key_id: Uuid) -> Result<(), DbError>;
@@ -455,7 +455,7 @@ pub trait InfraRepository: Send + Sync {
async fn get_reserved_signing_key_full(
&self,
public_key_did_key: &str,
public_key_did_key: &Did,
) -> Result<Option<ReservedSigningKeyFull>, DbError>;
async fn get_plc_tokens_by_did(&self, did: &Did) -> Result<Vec<PlcTokenInfo>, DbError>;
+3 -6
View File
@@ -233,11 +233,7 @@ pub trait SessionRepository: Send + Sync {
did: &Did,
) -> Result<u64, DbError>;
async fn delete_session_by_id(
&self,
session_id: SessionId,
did: &Did,
) -> Result<u64, DbError>;
async fn delete_session_by_id(&self, session_id: SessionId, did: &Did) -> Result<u64, DbError>;
async fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, DbError>;
@@ -300,7 +296,8 @@ pub trait SessionRepository: Send + Sync {
async fn update_mfa_verified(&self, did: &Did) -> Result<(), DbError>;
async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError>;
async fn get_app_password_hashes_by_did(&self, did: &Did)
-> Result<Vec<PasswordHash>, DbError>;
async fn refresh_session_atomic(
&self,
+5 -1
View File
@@ -224,7 +224,11 @@ pub trait UserRepository: Send + Sync {
async fn admin_update_handle(&self, did: &Did, handle: &Handle) -> Result<u64, DbError>;
async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError>;
async fn admin_update_password(
&self,
did: &Did,
password_hash: &PasswordHash,
) -> Result<u64, DbError>;
async fn set_admin_status(&self, did: &Did, is_admin: bool) -> Result<(), DbError>;
+4 -4
View File
@@ -531,7 +531,7 @@ impl InfraRepository for PostgresInfraRepository {
async fn reserve_signing_key(
&self,
did: Option<&Did>,
public_key_did_key: &str,
public_key_did_key: &Did,
private_key_bytes: &[u8],
expires_at: DateTime<Utc>,
) -> Result<Uuid, DbError> {
@@ -554,7 +554,7 @@ impl InfraRepository for PostgresInfraRepository {
async fn get_reserved_signing_key(
&self,
public_key_did_key: &str,
public_key_did_key: &Did,
) -> Result<Option<ReservedSigningKey>, DbError> {
let result = sqlx::query!(
r#"SELECT id, private_key_bytes
@@ -1123,7 +1123,7 @@ impl InfraRepository for PostgresInfraRepository {
async fn get_reserved_signing_key_full(
&self,
public_key_did_key: &str,
public_key_did_key: &Did,
) -> Result<Option<ReservedSigningKeyFull>, DbError> {
let row = sqlx::query!(
r#"SELECT id, did, public_key_did_key, private_key_bytes, expires_at, used_at
@@ -1137,7 +1137,7 @@ impl InfraRepository for PostgresInfraRepository {
Ok(row.map(|r| ReservedSigningKeyFull {
id: r.id,
did: r.did.map(|d| Did::new(d).expect("valid DID in database")),
public_key_did_key: r.public_key_did_key,
public_key_did_key: Did::from(r.public_key_did_key),
private_key_bytes: r.private_key_bytes,
expires_at: r.expires_at,
used_at: r.used_at,
+5 -6
View File
@@ -131,11 +131,7 @@ impl SessionRepository for PostgresSessionRepository {
Ok(result.rows_affected())
}
async fn delete_session_by_id(
&self,
session_id: SessionId,
did: &Did,
) -> Result<u64, DbError> {
async fn delete_session_by_id(&self, session_id: SessionId, did: &Did) -> Result<u64, DbError> {
let result = sqlx::query!(
"DELETE FROM session_tokens WHERE id = $1 AND did = $2",
session_id.as_i32(),
@@ -499,7 +495,10 @@ impl SessionRepository for PostgresSessionRepository {
Ok(())
}
async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError> {
async fn get_app_password_hashes_by_did(
&self,
did: &Did,
) -> Result<Vec<PasswordHash>, DbError> {
let rows = sqlx::query_scalar!(
r#"SELECT ap.password_hash FROM app_passwords ap
JOIN users u ON ap.user_id = u.id
+5 -1
View File
@@ -651,7 +651,11 @@ impl UserRepository for PostgresUserRepository {
Ok(result.rows_affected())
}
async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError> {
async fn admin_update_password(
&self,
did: &Did,
password_hash: &PasswordHash,
) -> Result<u64, DbError> {
let result = sqlx::query!(
"UPDATE users SET password_hash = $1 WHERE did = $2",
password_hash.as_str(),
+15 -18
View File
@@ -60,9 +60,9 @@ pub enum ResolveError {
#[error("no DID found in DNS TXT records for {domain}")]
NoDid { domain: String },
#[error("DID document fetch failed for {did}: {reason}")]
DidResolution { did: String, reason: String },
DidResolution { did: Did, reason: String },
#[error("no PDS endpoint found in DID document for {did}")]
NoPdsEndpoint { did: String },
NoPdsEndpoint { did: Did },
#[error("schema fetch failed from {url}: {reason}")]
SchemaFetch { url: String, reason: String },
#[error("schema deserialization failed: {0}")]
@@ -82,22 +82,21 @@ pub fn nsid_to_authority(nsid: &Nsid) -> String {
Ok(segments.join("."))
}
pub async fn resolve_did_from_dns(authority: &str) -> Result<String, ResolveError> {
pub async fn resolve_did_from_dns(authority: &str) -> Result<Did, ResolveError> {
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or_else(|e| {
tracing::warn!("falling back to default DNS resolvers: {}", e);
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())
});
let extract_did = |lookup: hickory_resolver::lookup::TxtLookup| -> Option<String> {
let extract_did = |lookup: hickory_resolver::lookup::TxtLookup| -> Option<Did> {
lookup
.iter()
.flat_map(|record| record.txt_data())
.find_map(|txt| {
let txt_str = String::from_utf8_lossy(txt);
txt_str.strip_prefix("did=").and_then(|did| {
let did = did.trim();
did.starts_with("did:").then(|| did.to_string())
})
txt_str
.strip_prefix("did=")
.and_then(|did| Did::new(did.trim()).ok())
})
};
@@ -124,7 +123,7 @@ pub async fn resolve_did_from_dns(authority: &str) -> Result<String, ResolveErro
}
pub async fn resolve_pds_endpoint(
did: &str,
did: &Did,
plc_directory_url: Option<&str>,
) -> Result<String, ResolveError> {
let plc_base = plc_directory_url.unwrap_or(DEFAULT_PLC_DIRECTORY);
@@ -137,7 +136,7 @@ pub async fn resolve_pds_endpoint(
Some(("web", domain)) => format!("https://{}/.well-known/did.json", domain),
_ => {
return Err(ResolveError::DidResolution {
did: did.to_string(),
did: did.clone(),
reason: "unsupported DID method".to_string(),
});
}
@@ -148,26 +147,24 @@ pub async fn resolve_pds_endpoint(
.send()
.await
.map_err(|e| ResolveError::DidResolution {
did: did.to_string(),
did: did.clone(),
reason: e.to_string(),
})?;
let body = read_body_limited(resp, MAX_RESPONSE_BYTES)
.await
.map_err(|reason| ResolveError::DidResolution {
did: did.to_string(),
did: did.clone(),
reason,
})?;
let doc: serde_json::Value =
serde_json::from_slice(&body).map_err(|e| ResolveError::DidResolution {
did: did.to_string(),
did: did.clone(),
reason: e.to_string(),
})?;
extract_pds_endpoint(&doc).ok_or(ResolveError::NoPdsEndpoint {
did: did.to_string(),
})
extract_pds_endpoint(&doc).ok_or_else(|| ResolveError::NoPdsEndpoint { did: did.clone() })
}
fn extract_pds_endpoint(doc: &serde_json::Value) -> Option<String> {
@@ -188,7 +185,7 @@ fn extract_pds_endpoint(doc: &serde_json::Value) -> Option<String> {
pub async fn fetch_schema_from_pds(
pds_endpoint: &str,
did: &str,
did: &Did,
nsid: &Nsid,
) -> Result<LexiconDoc, ResolveError> {
let url = format!(
@@ -280,7 +277,7 @@ pub async fn resolve_lexicon_with_config(
pub async fn resolve_lexicon_from_did(
nsid: &Nsid,
did: &str,
did: &Did,
plc_directory_url: Option<&str>,
) -> Result<LexiconDoc, ResolveError> {
let pds_endpoint = resolve_pds_endpoint(did, plc_directory_url).await?;
@@ -71,7 +71,7 @@ async fn test_resolve_pds_endpoint_from_plc() {
.mount(&plc_server)
.await;
let endpoint = resolve_pds_endpoint(did, Some(&plc_server.uri()))
let endpoint = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri()))
.await
.unwrap();
assert_eq!(endpoint, "https://pds.example.com");
@@ -94,7 +94,7 @@ async fn test_resolve_pds_endpoint_no_pds_service() {
.mount(&plc_server)
.await;
let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await;
let result = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())).await;
assert!(matches!(result, Err(ResolveError::NoPdsEndpoint { .. })));
}
@@ -109,13 +109,13 @@ async fn test_resolve_pds_endpoint_plc_not_found() {
.mount(&plc_server)
.await;
let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await;
let result = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_resolve_pds_endpoint_unsupported_did_method() {
let result = resolve_pds_endpoint("did:key:z6MkTest", None).await;
let result = resolve_pds_endpoint(&"did:key:z6MkTest".parse().unwrap(), None).await;
assert!(matches!(result, Err(ResolveError::DidResolution { .. })));
}
@@ -146,7 +146,7 @@ async fn test_resolve_pds_endpoint_multiple_services_picks_pds() {
.mount(&plc_server)
.await;
let endpoint = resolve_pds_endpoint(did, Some(&plc_server.uri()))
let endpoint = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri()))
.await
.unwrap();
assert_eq!(endpoint, "https://pds.example.com");
@@ -402,6 +402,6 @@ async fn test_plc_server_timeout() {
.mount(&plc_server)
.await;
let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await;
let result = resolve_pds_endpoint(&did.parse().unwrap(), Some(&plc_server.uri())).await;
assert!(result.is_err());
}
@@ -19,7 +19,7 @@ pub struct ConsentResponse {
pub logo_uri: Option<String>,
pub scopes: Vec<ScopeInfo>,
pub show_consent: bool,
pub did: String,
pub did: Did,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -256,7 +256,7 @@ pub async fn consent_get(
logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()),
scopes,
show_consent,
did: did.to_string(),
did: did.clone(),
handle: account_handle,
is_delegation,
controller_did: controller_did_resp,
@@ -224,11 +224,7 @@ pub async fn delegation_auth(
.flatten();
if is_cross_pds || controller_local.is_none() {
let did_doc = match state
.plc_client()
.get_document(controller_did.as_str())
.await
{
let did_doc = match state.plc_client().get_document(&controller_did).await {
Ok(doc) => doc,
Err(_) => {
return DelegationAuthResponse::err("Failed to resolve controller DID");
@@ -65,7 +65,7 @@ pub async fn handle_authorization_code_grant(
{
return Err(OAuthError::InvalidGrant("client_id mismatch".to_string()));
}
let did = authorized.did.to_string();
let did = authorized.did.clone();
let client_metadata_cache = ClientMetadataCache::new(3600);
let client_metadata = client_metadata_cache.get(&authorized.client_id).await?;
let client_auth = match &request.client_auth {
@@ -134,16 +134,10 @@ pub async fn handle_authorization_code_grant(
let now = Utc::now();
let (raw_scope, controller_did) = if let Some(ref controller) = authorized.controller_did {
let did_parsed: Did = did
.parse()
.map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?;
let controller_parsed: Did = controller
.parse()
.map_err(|_| OAuthError::InvalidRequest("Invalid controller DID format".to_string()))?;
let grant = state
.repos
.delegation
.get_delegation(&did_parsed, &controller_parsed)
.get_delegation(&did, controller)
.await
.ok()
.flatten();
@@ -179,7 +173,7 @@ pub async fn handle_authorization_code_grant(
&did,
dpop_jkt.as_deref(),
final_scope.as_deref(),
controller_did.as_deref(),
controller_did.as_ref(),
)?;
let stored_client_auth = authorized.client_auth.unwrap_or(ClientAuth::None);
let refresh_expiry_days = if matches!(stored_client_auth, ClientAuth::None) {
@@ -189,11 +183,8 @@ pub async fn handle_authorization_code_grant(
};
let mut stored_parameters = authorized.parameters.clone();
stored_parameters.dpop_jkt = dpop_jkt.clone();
let did_typed: Did = did
.parse()
.map_err(|_| OAuthError::InvalidRequest("Invalid DID format".to_string()))?;
let token_data = TokenData {
did: did_typed,
did: did.clone(),
token_id: token_id.clone(),
created_at: now,
updated_at: now,
@@ -295,11 +286,11 @@ pub async fn handle_refresh_token_grant(
);
let dpop_jkt = token_data.parameters.dpop_jkt.as_deref();
let access_token = create_access_token_with_delegation(
&token_data.token_id.0,
token_data.did.as_str(),
&token_data.token_id,
&token_data.did,
dpop_jkt,
token_data.scope.as_deref(),
token_data.controller_did.as_ref().map(|d| d.as_str()),
token_data.controller_did.as_ref(),
)?;
let mut response_headers = HeaderMap::new();
let config = AuthConfig::get();
@@ -320,7 +311,7 @@ pub async fn handle_refresh_token_grant(
expires_in: ACCESS_TOKEN_EXPIRY_SECONDS,
refresh_token: token_data.current_refresh_token,
scope: token_data.scope,
sub: Some(token_data.did.to_string()),
sub: Some(token_data.did),
}),
));
}
@@ -409,11 +400,11 @@ pub async fn handle_refresh_token_grant(
"Refresh token rotated successfully"
);
let access_token = create_access_token_with_delegation(
&token_data.token_id.0,
token_data.did.as_str(),
dpop_jkt.as_deref(),
&token_data.token_id,
&token_data.did,
dpop_jkt.as_ref(),
token_data.scope.as_deref(),
token_data.controller_did.as_ref().map(|d| d.as_str()),
token_data.controller_did.as_ref(),
)?;
let mut response_headers = HeaderMap::new();
let config = AuthConfig::get();
@@ -434,7 +425,7 @@ pub async fn handle_refresh_token_grant(
expires_in: ACCESS_TOKEN_EXPIRY_SECONDS,
refresh_token: Some(new_refresh_token),
scope: token_data.scope,
sub: Some(token_data.did.to_string()),
sub: Some(token_data.did),
}),
))
}
@@ -35,16 +35,7 @@ pub fn create_access_token_with_delegation(
sub: &tranquil_types::Did,
dpop_jkt: Option<&tranquil_types::JwkThumbprint>,
scope: Option<&str>,
) -> Result<String, OAuthError> {
create_access_token_with_delegation(session_id, sub, dpop_jkt, scope, None)
}
pub fn create_access_token_with_delegation(
session_id: &str,
sub: &str,
dpop_jkt: Option<&str>,
scope: Option<&str>,
controller_did: Option<&str>,
controller_did: Option<&tranquil_types::Did>,
) -> Result<String, OAuthError> {
use serde_json::json;
let jti = uuid::Uuid::new_v4().to_string();
@@ -184,5 +184,5 @@ pub struct TokenResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
pub sub: Option<tranquil_types::Did>,
}
@@ -839,7 +839,7 @@ pub struct CompleteRegistrationInput {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompleteRegistrationResponse {
pub did: String,
pub did: tranquil_pds::types::Did,
pub handle: tranquil_pds::types::Handle,
pub redirect_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1016,13 +1016,14 @@ pub async fn complete_registration(
let pds_endpoint = format!("https://{}", hostname);
let did_type = input.did_type.as_deref().unwrap_or("plc");
let did = match did_type {
let did: tranquil_pds::types::Did = match did_type {
"web" => {
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
return Err(ApiError::SelfHostedDidWebDisabled);
}
let encoded_handle = handle.replace(':', "%3A");
let self_hosted_did = format!("did:web:{}", encoded_handle);
let self_hosted_did =
tranquil_pds::types::Did::from(format!("did:web:{}", encoded_handle));
tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account");
self_hosted_did
}
@@ -1094,11 +1095,8 @@ pub async fn complete_registration(
};
let rev = Tid::now(LimitedU32::MIN);
let did_typed: tranquil_pds::types::Did = did
.parse()
.map_err(|_| ApiError::InternalError(Some("Invalid DID".into())))?;
let (commit_bytes, _sig) = match tranquil_pds::repo_ops::create_signed_commit(
&did_typed,
&did,
mst_root,
&rev,
None,
@@ -1133,7 +1131,7 @@ pub async fn complete_registration(
let create_input = tranquil_db_traits::CreateSsoAccountInput {
handle: handle.clone(),
email: email.clone(),
did: did_typed.clone(),
did: did.clone(),
preferred_comms_channel: verification_channel,
discord_username: input
.discord_username
@@ -1200,7 +1198,7 @@ pub async fn complete_registration(
}
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
&state,
&did_typed,
&did,
tranquil_db_traits::AccountStatus::Active,
)
.await
@@ -1214,7 +1212,7 @@ pub async fn complete_registration(
});
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
&state,
&did_typed,
&did,
&tranquil_pds::types::PROFILE_COLLECTION,
&tranquil_pds::types::PROFILE_RKEY,
&profile_record,
@@ -1269,12 +1267,7 @@ pub async fn complete_registration(
"SSO registration completed successfully"
);
let user_id = state
.repos
.user
.get_id_by_did(&did_typed)
.await
.unwrap_or(None);
let user_id = state.repos.user.get_id_by_did(&did).await.unwrap_or(None);
let channel_auto_verified = verification_channel == tranquil_db_traits::CommsChannel::Email
&& pending_preview.provider_email_verified
@@ -1284,7 +1277,7 @@ pub async fn complete_registration(
let _ = state
.repos
.user
.set_channel_verified(&did_typed, tranquil_db_traits::CommsChannel::Email)
.set_channel_verified(&did, tranquil_db_traits::CommsChannel::Email)
.await;
tracing::info!(did = %did, "Auto-verified email from SSO provider");
@@ -1318,7 +1311,7 @@ pub async fn complete_registration(
};
let session_data = tranquil_db_traits::SessionTokenCreate {
did: did_typed.clone(),
did: did.clone(),
access_jti: access_meta.jti.clone(),
refresh_jti: refresh_meta.jti.clone(),
access_expires_at: access_meta.expires_at,
@@ -1373,7 +1366,7 @@ pub async fn complete_registration(
if let Some(uid) = user_id {
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
&did_typed,
&did,
verification_channel,
&verification_recipient,
);
+1 -1
View File
@@ -257,7 +257,7 @@ pub struct TokenResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
pub sub: Option<Did>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+11 -4
View File
@@ -111,7 +111,7 @@ fn is_protected_method(method: &str) -> bool {
}
/// Fetch the `feed` generator record from the AppView and return its `did`.
async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> Option<String> {
async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> Option<Did> {
#[derive(serde::Deserialize)]
struct GetFeedQuery {
feed: String,
@@ -136,7 +136,10 @@ async fn resolve_feed_generator_did(appview_url: &str, query: Option<&str>) -> O
return None;
}
let body: serde_json::Value = resp.json().await.ok()?;
body.get("value")?.get("did")?.as_str().map(str::to_string)
body.get("value")?
.get("did")?
.as_str()
.and_then(|s| Did::new(s).ok())
}
pub struct XrpcProxyLayer {
@@ -241,7 +244,11 @@ async fn proxy_handler(
)
.into_response();
};
let Ok(resolved) = state.did_resolver.resolve_service(did, service_id).await else {
let Ok(did) = did.parse::<Did>() else {
return ApiError::InvalidRequest("Invalid DID in atproto-proxy header".into())
.into_response();
};
let Ok(resolved) = state.did_resolver.resolve_service(&did, service_id).await else {
error!(did = %did, service_id = %service_id, "Could not resolve service DID");
return ApiError::UpstreamFailure.into_response();
};
@@ -340,7 +347,7 @@ async fn proxy_handler(
}
}
} else {
(resolved.did.clone(), method)
(resolved.did.clone(), method_nsid.clone())
};
match crate::auth::create_service_token(
+28 -27
View File
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::cache::Cache;
use crate::types::Did;
use crate::util::{generate_token_code, normalize_token_code};
const TOKEN_TTL_SECS: u64 = 900;
@@ -41,7 +42,7 @@ pub enum TokenError {
ExpiredToken,
}
fn cache_key(did: &str, purpose: EmailTokenPurpose) -> String {
fn cache_key(did: &Did, purpose: EmailTokenPurpose) -> String {
format!("email_token:{}:{}", purpose.as_str(), did)
}
@@ -51,7 +52,7 @@ fn current_timestamp() -> u64 {
pub async fn create_email_token(
cache: &dyn Cache,
did: &str,
did: &Did,
purpose: EmailTokenPurpose,
) -> Result<String, TokenError> {
if !cache.is_available() {
@@ -80,7 +81,7 @@ pub async fn create_email_token(
pub async fn validate_email_token(
cache: &dyn Cache,
did: &str,
did: &Did,
purpose: EmailTokenPurpose,
token: &str,
) -> Result<(), TokenError> {
@@ -110,7 +111,7 @@ pub async fn validate_email_token(
Ok(())
}
pub async fn delete_email_token(cache: &dyn Cache, did: &str, purpose: EmailTokenPurpose) {
pub async fn delete_email_token(cache: &dyn Cache, did: &Did, purpose: EmailTokenPurpose) {
let _ = cache.delete(&cache_key(did, purpose)).await;
}
@@ -188,9 +189,9 @@ mod tests {
#[tokio::test]
async fn test_create_and_validate_token() {
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail)
let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail)
.await
.unwrap();
@@ -198,53 +199,53 @@ mod tests {
assert!(token.contains('-'));
let result =
validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &token).await;
validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &token).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_token_consumed_after_use() {
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail)
let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail)
.await
.unwrap();
validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &token)
validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &token)
.await
.unwrap();
let result =
validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &token).await;
validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &token).await;
assert_eq!(result.unwrap_err(), TokenError::InvalidToken);
}
#[tokio::test]
async fn test_invalid_token_rejected() {
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
let _token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail)
let _token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail)
.await
.unwrap();
let result =
validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, "XXXXX-XXXXX").await;
validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, "XXXXX-XXXXX").await;
assert_eq!(result.unwrap_err(), TokenError::InvalidToken);
}
#[tokio::test]
async fn test_wrong_purpose_rejected() {
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail)
let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail)
.await
.unwrap();
let result =
validate_email_token(&cache, did, EmailTokenPurpose::ConfirmEmail, &token).await;
validate_email_token(&cache, &did, EmailTokenPurpose::ConfirmEmail, &token).await;
assert_eq!(result.unwrap_err(), TokenError::InvalidToken);
}
@@ -252,11 +253,11 @@ mod tests {
async fn test_token_format() {
// The emitted token is the display form: uppercase `XXXXX-XXXXX`.
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
(0..50).for_each(|_| {
let token = futures::executor::block_on(create_email_token(
&cache,
did,
&did,
EmailTokenPurpose::UpdateEmail,
))
.unwrap();
@@ -269,39 +270,39 @@ mod tests {
#[tokio::test]
async fn test_case_insensitive_validation() {
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail)
let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail)
.await
.unwrap();
let lowercase = token.to_lowercase();
let result =
validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &lowercase).await;
validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &lowercase).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_hyphen_insensitive_validation() {
let cache = MockCache::new();
let did = "did:plc:test123";
let did = Did::from("did:plc:teq".to_string());
let token = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail)
let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail)
.await
.unwrap();
let no_hyphen = token.replace('-', "");
let result =
validate_email_token(&cache, did, EmailTokenPurpose::UpdateEmail, &no_hyphen).await;
validate_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail, &no_hyphen).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_noop_cache_returns_unavailable() {
let cache = crate::cache::NoOpCache;
let did = "did:plc:test";
let did = Did::from("did:plc:whelk".to_string());
let result = create_email_token(&cache, did, EmailTokenPurpose::UpdateEmail).await;
let result = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail).await;
assert_eq!(result.unwrap_err(), TokenError::CacheUnavailable);
}
}
+8 -8
View File
@@ -58,13 +58,13 @@ pub async fn create_challenge(
}
pub async fn clear_challenge(cache: &dyn Cache, did: &Did) {
let _ = cache.delete(&challenge_key(did.as_str())).await;
let _ = cache.delete(&cooldown_key(did.as_str())).await;
let _ = cache.delete(&challenge_key(did)).await;
let _ = cache.delete(&cooldown_key(did)).await;
}
async fn validate_challenge_internal(
cache: &dyn Cache,
did: &str,
did: &Did,
code: &str,
) -> Result<(), ValidationError> {
if !cache.is_available() {
@@ -119,11 +119,11 @@ async fn validate_challenge_internal(
Ok(())
}
fn challenge_key(did: &str) -> String {
fn challenge_key(did: &Did) -> String {
format!("legacy_2fa:{}", did)
}
fn cooldown_key(did: &str) -> String {
fn cooldown_key(did: &Did) -> String {
format!("legacy_2fa_cooldown:{}", did)
}
@@ -215,7 +215,7 @@ pub async fn validate_challenge(
did: &Did,
code: &str,
) -> Result<(), ValidationError> {
validate_challenge_internal(cache, did.as_str(), code).await
validate_challenge_internal(cache, did, code).await
}
async fn create_challenge_code(
@@ -226,7 +226,7 @@ async fn create_challenge_code(
return Err(ChallengeError::CacheUnavailable);
}
let cooldown = cooldown_key(did.as_str());
let cooldown = cooldown_key(did);
if cache.get(&cooldown).await.is_some() {
return Err(ChallengeError::RateLimited);
}
@@ -244,7 +244,7 @@ async fn create_challenge_code(
cache
.set(
&challenge_key(did.as_str()),
&challenge_key(did),
&json,
Duration::from_secs(CHALLENGE_TTL_SECS),
)
+4 -13
View File
@@ -372,7 +372,7 @@ async fn validate_bearer_token_with_options_internal(
)
.await;
let status_cache_key = crate::cache_keys::user_status_key(did.as_ref());
let status_cache_key = crate::cache_keys::user_status_key(&did);
let cached = CachedUserStatus {
deactivated: user.deactivated_at.is_some(),
takendown: user.takedown_ref.is_some(),
@@ -504,22 +504,13 @@ async fn validate_bearer_token_with_options_internal(
oauth_token.key_bytes.as_deref(),
oauth_token.encryption_version,
);
let did: Did = oauth_token
.did
.parse()
.map_err(|_| TokenValidationError::InvalidToken)?;
let controller_did: Option<Did> = oauth_info
.controller_did
.map(|d| d.parse())
.transpose()
.map_err(|_| TokenValidationError::InvalidToken)?;
return Ok(AuthenticatedUser {
did,
did: oauth_token.did,
key_bytes,
is_admin: oauth_token.is_admin,
status,
scope: oauth_info.scope,
controller_did,
controller_did: oauth_info.controller_did,
auth_source: AuthSource::OAuth,
});
} else {
@@ -530,7 +521,7 @@ async fn validate_bearer_token_with_options_internal(
Err(TokenValidationError::AuthenticationFailed)
}
pub async fn invalidate_auth_cache(cache: &dyn Cache, did: &str) {
pub async fn invalidate_auth_cache(cache: &dyn Cache, did: &Did) {
let key_cache_key = crate::cache_keys::signing_key_key(did);
let status_cache_key = crate::cache_keys::user_status_key(did);
let _ = cache.delete(&key_cache_key).await;
+12 -8
View File
@@ -219,7 +219,7 @@ impl ServiceTokenVerifier {
}
}
let did = claims.iss.as_str();
let did = &claims.iss;
let public_key = self.resolve_signing_key(did).await?;
let signature_bytes = URL_SAFE_NO_PAD
@@ -240,7 +240,7 @@ impl ServiceTokenVerifier {
Ok(claims)
}
async fn resolve_signing_key(&self, did: &str) -> Result<VerifyingKey, ServiceTokenError> {
async fn resolve_signing_key(&self, did: &Did) -> Result<VerifyingKey, ServiceTokenError> {
let did_doc = self.resolve_did_document(did).await?;
let atproto_key = did_doc
@@ -257,18 +257,22 @@ impl ServiceTokenVerifier {
parse_did_key_multibase(multibase)
}
async fn resolve_did_document(&self, did: &str) -> Result<FullDidDocument, ServiceTokenError> {
if did.starts_with("did:plc:") {
async fn resolve_did_document(&self, did: &Did) -> Result<FullDidDocument, ServiceTokenError> {
if did.is_plc() {
self.resolve_did_plc(did).await
} else if did.starts_with("did:web:") {
} else if did.is_web() {
self.resolve_did_web(did).await
} else {
Err(ServiceTokenError::UnsupportedDidMethod)
}
}
async fn resolve_did_plc(&self, did: &str) -> Result<FullDidDocument, ServiceTokenError> {
let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did));
async fn resolve_did_plc(&self, did: &Did) -> Result<FullDidDocument, ServiceTokenError> {
let url = format!(
"{}/{}",
self.plc_directory_url,
urlencoding::encode(did.as_str())
);
debug!("Resolving did:plc {} via {}", did, url);
let resp = self
@@ -291,7 +295,7 @@ impl ServiceTokenVerifier {
.map_err(ServiceTokenError::InvalidDidDocument)
}
async fn resolve_did_web(&self, did: &str) -> Result<FullDidDocument, ServiceTokenError> {
async fn resolve_did_web(&self, did: &Did) -> Result<FullDidDocument, ServiceTokenError> {
let host = did
.strip_prefix("did:web:")
.ok_or(ServiceTokenError::InvalidFormat)?;
+7 -7
View File
@@ -4,11 +4,11 @@ pub fn session_key(did: &Did, jti: &Jti) -> String {
format!("auth:session:{}:{}", did, jti)
}
pub fn signing_key_key(did: &str) -> String {
pub fn signing_key_key(did: &Did) -> String {
format!("auth:key:{}", did)
}
pub fn user_status_key(did: &str) -> String {
pub fn user_status_key(did: &Did) -> String {
format!("auth:status:{}", did)
}
@@ -16,19 +16,19 @@ pub fn handle_key(handle: &Handle) -> String {
format!("handle:{}", handle)
}
pub fn reauth_key(did: &str) -> String {
pub fn reauth_key(did: &Did) -> String {
format!("reauth:{}", did)
}
pub fn plc_doc_key(did: &str) -> String {
pub fn plc_doc_key(did: &Did) -> String {
format!("plc:doc:{}", did)
}
pub fn plc_data_key(did: &str) -> String {
pub fn plc_data_key(did: &Did) -> String {
format!("plc:data:{}", did)
}
pub fn email_update_key(did: &str) -> String {
pub fn email_update_key(did: &Did) -> String {
format!("email_update:{}", did)
}
@@ -36,6 +36,6 @@ pub fn scope_ref_key(cid: &CidLink) -> String {
format!("scope_ref:{}", cid)
}
pub fn auto_verify_sent_key(did: &str) -> String {
pub fn auto_verify_sent_key(did: &Did) -> String {
format!("auto_verify_sent:{}", did)
}
+1 -1
View File
@@ -38,7 +38,7 @@ pub async fn resolve_identity(
.flatten()
.is_some();
let did_doc = state.did_resolver.resolve_did(did.as_str()).await?;
let did_doc = state.did_resolver.resolve_did(did).await?;
let pds_url = did_doc.services.iter().find_map(|svc| {
if (svc.id == "#atproto_pds" || svc.id.ends_with("#atproto_pds"))
+39 -30
View File
@@ -1,3 +1,4 @@
use crate::types::Did;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -30,7 +31,7 @@ pub enum ServiceResolutionError {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidDocument {
pub id: String,
pub id: Did,
#[serde(default)]
#[serde(rename = "service")]
pub services: Vec<DidService>,
@@ -51,7 +52,7 @@ pub struct DidService {
#[derive(Debug, Clone)]
pub struct ResolvedService {
pub url: String,
pub did: String,
pub did: Did,
pub service_id: String,
}
@@ -94,7 +95,7 @@ impl DidResolver {
pub async fn resolve_service(
&self,
did: &str,
did: &Did,
service_id: &str,
) -> Result<Arc<ResolvedService>, ServiceResolutionError> {
{
@@ -117,7 +118,7 @@ impl DidResolver {
let resolved = Arc::new(ResolvedService {
url: service.service_endpoint.clone(),
did: did.into(),
did: did.clone(),
service_id: service_id.into(),
});
@@ -132,10 +133,10 @@ impl DidResolver {
Ok(resolved)
}
pub async fn resolve_did(&self, did: &str) -> Result<Arc<DidDocument>, DidResolutionError> {
pub async fn resolve_did(&self, did: &Did) -> Result<Arc<DidDocument>, DidResolutionError> {
{
let cache = self.parsed_did_doc_cache.read().await;
if let Some(cached) = cache.get(did)
if let Some(cached) = cache.get(did.as_str())
&& cached.0.elapsed() < self.cache_ttl
{
return Ok(cached.1.clone());
@@ -146,34 +147,34 @@ impl DidResolver {
{
let mut cache = self.parsed_did_doc_cache.write().await;
cache.insert(did.into(), (Instant::now(), resolved.clone()));
cache.insert(did.as_str().into(), (Instant::now(), resolved.clone()));
}
Ok(resolved)
}
pub async fn refresh_did(&self, did: &str) -> Result<Arc<DidDocument>, DidResolutionError> {
pub async fn refresh_did(&self, did: &Did) -> Result<Arc<DidDocument>, DidResolutionError> {
{
let mut cache = self.parsed_did_doc_cache.write().await;
cache.remove(did);
cache.remove(did.as_str());
let mut cache = self.service_cache.write().await;
cache.retain(|k, _| !k.starts_with(did));
cache.retain(|k, _| !k.starts_with(did.as_str()));
}
self.resolve_did(did).await
}
async fn resolve_did_uncached(&self, did: &str) -> Result<DidDocument, DidResolutionError> {
if did.starts_with("did:web:") {
async fn resolve_did_uncached(&self, did: &Did) -> Result<DidDocument, DidResolutionError> {
if did.is_web() {
self.resolve_did_web(did).await
} else if did.starts_with("did:plc:") {
} else if did.is_plc() {
self.resolve_did_plc(did).await
} else {
warn!("Unsupported DID method: {}", did);
Err(DidResolutionError::UnsupportedDidMethod(did.into()))
Err(DidResolutionError::UnsupportedDidMethod(did.to_string()))
}
}
async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, DidResolutionError> {
async fn resolve_did_web(&self, did: &Did) -> Result<DidDocument, DidResolutionError> {
let url = build_did_web_url(did)?;
debug!("Resolving did:web {} via {}", did, url);
@@ -197,8 +198,12 @@ impl DidResolver {
.map_err(|e| DidResolutionError::InvalidDocument(e.to_string()))
}
async fn resolve_did_plc(&self, did: &str) -> Result<DidDocument, DidResolutionError> {
let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did));
async fn resolve_did_plc(&self, did: &Did) -> Result<DidDocument, DidResolutionError> {
let url = format!(
"{}/{}",
self.plc_directory_url,
urlencoding::encode(did.as_str())
);
debug!("Resolving did:plc {} via {}", did, url);
@@ -227,11 +232,11 @@ impl DidResolver {
pub async fn fetch_did_document(
&self,
did: &str,
did: &Did,
) -> Result<Arc<serde_json::Value>, DidResolutionError> {
{
let cache = self.did_doc_cache.read().await;
if let Some(cached) = cache.get(did)
if let Some(cached) = cache.get(did.as_str())
&& cached.0.elapsed() < self.cache_ttl
{
return Ok(cached.1.clone());
@@ -242,7 +247,7 @@ impl DidResolver {
{
let mut cache = self.did_doc_cache.write().await;
cache.insert(did.into(), (Instant::now(), resolved.clone()));
cache.insert(did.as_str().into(), (Instant::now(), resolved.clone()));
}
Ok(resolved)
@@ -251,21 +256,21 @@ impl DidResolver {
// TODO: make cached version
async fn fetch_did_document_uncached(
&self,
did: &str,
did: &Did,
) -> Result<serde_json::Value, DidResolutionError> {
if did.starts_with("did:web:") {
if did.is_web() {
self.fetch_did_document_web(did).await
} else if did.starts_with("did:plc:") {
} else if did.is_plc() {
self.fetch_did_document_plc(did).await
} else {
warn!("Unsupported DID method: {}", did);
Err(DidResolutionError::UnsupportedDidMethod(did.into()))
Err(DidResolutionError::UnsupportedDidMethod(did.to_string()))
}
}
async fn fetch_did_document_web(
&self,
did: &str,
did: &Did,
) -> Result<serde_json::Value, DidResolutionError> {
let url = build_did_web_url(did)?;
@@ -290,9 +295,13 @@ impl DidResolver {
async fn fetch_did_document_plc(
&self,
did: &str,
did: &Did,
) -> Result<serde_json::Value, DidResolutionError> {
let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did));
let url = format!(
"{}/{}",
self.plc_directory_url,
urlencoding::encode(did.as_str())
);
let resp = self
.client
@@ -317,9 +326,9 @@ impl DidResolver {
.map_err(|e| DidResolutionError::InvalidDocument(e.to_string()))
}
pub async fn invalidate_cache(&self, did: &str) {
pub async fn invalidate_cache(&self, did: &Did) {
let mut doc_cache = self.parsed_did_doc_cache.write().await;
doc_cache.remove(did);
doc_cache.remove(did.as_str());
}
}
@@ -333,7 +342,7 @@ pub fn create_did_resolver() -> Arc<DidResolver> {
Arc::new(DidResolver::new())
}
fn build_did_web_url(did: &str) -> Result<String, DidResolutionError> {
fn build_did_web_url(did: &Did) -> Result<String, DidResolutionError> {
let host = did
.strip_prefix("did:web:")
.ok_or(DidResolutionError::InvalidDidWeb)?;
+6 -12
View File
@@ -16,7 +16,7 @@ pub enum HandleResolutionError {
#[error("Invalid DID format in record")]
InvalidDid,
#[error("DID mismatch: expected {expected}, got {actual}")]
DidMismatch { expected: String, actual: String },
DidMismatch { expected: Did, actual: Did },
}
pub async fn resolve_handle_dns(handle: &Handle) -> Result<Did, HandleResolutionError> {
@@ -34,10 +34,9 @@ pub async fn resolve_handle_dns(handle: &Handle) -> Result<Did, HandleResolution
.flat_map(|record| record.txt_data())
.find_map(|txt| {
let txt_str = String::from_utf8_lossy(txt);
txt_str.strip_prefix("did=").and_then(|did| {
let did = did.trim();
did.starts_with("did:").then(|| did.to_string())
})
txt_str
.strip_prefix("did=")
.and_then(|did| Did::new(did.trim()).ok())
})
.ok_or(HandleResolutionError::NotFound)
}
@@ -58,12 +57,7 @@ pub async fn resolve_handle_http(handle: &Handle) -> Result<Did, HandleResolutio
.text()
.await
.map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
let did = body.trim();
if did.starts_with("did:") {
Ok(did.to_string())
} else {
Err(HandleResolutionError::InvalidDid)
}
Did::new(body.trim()).map_err(|_| HandleResolutionError::InvalidDid)
}
pub async fn resolve_handle(handle: &Handle) -> Result<Did, HandleResolutionError> {
@@ -78,7 +72,7 @@ pub async fn resolve_handle(handle: &Handle) -> Result<Did, HandleResolutionErro
pub async fn verify_handle_ownership(
handle: &Handle,
expected_did: &str,
expected_did: &Did,
) -> Result<(), HandleResolutionError> {
let resolved_did = resolve_handle(handle).await?;
if resolved_did == expected_did {
+2 -2
View File
@@ -110,11 +110,11 @@ impl CrossPdsOAuthClient {
})
}
pub async fn check_remote_is_delegated(&self, pds_url: &str, did: &str) -> Option<bool> {
pub async fn check_remote_is_delegated(&self, pds_url: &str, did: &Did) -> Option<bool> {
let url = format!(
"{}/oauth/security-status?identifier={}",
pds_url.trim_end_matches('/'),
urlencoding::encode(did)
urlencoding::encode(did.as_str())
);
let resp = self.http.get(&url).send().await.ok()?;
if !resp.status().is_success() {
+1 -5
View File
@@ -93,12 +93,8 @@ pub async fn verify_oauth_access_token(
));
}
}
let did: Did = token_data
.did
.parse()
.map_err(|_| OAuthError::InvalidToken("Invalid DID in token".to_string()))?;
Ok(VerifyResult {
did,
did: token_data.did,
token_id,
client_id: token_data.client_id,
scope: token_data.scope,
+10 -10
View File
@@ -202,11 +202,11 @@ impl PlcClient {
}
}
fn encode_did(did: &str) -> String {
urlencoding::encode(did).to_string()
fn encode_did(did: &Did) -> String {
urlencoding::encode(did.as_str()).to_string()
}
pub async fn get_document(&self, did: &str) -> Result<Value, PlcError> {
pub async fn get_document(&self, did: &Did) -> Result<Value, PlcError> {
let cache_key = crate::cache_keys::plc_doc_key(did);
if let Some(ref cache) = self.cache
&& let Some(cached) = cache.get(&cache_key).await
@@ -245,7 +245,7 @@ impl PlcClient {
Ok(value)
}
pub async fn get_document_data(&self, did: &str) -> Result<Value, PlcError> {
pub async fn get_document_data(&self, did: &Did) -> Result<Value, PlcError> {
let cache_key = crate::cache_keys::plc_data_key(did);
if let Some(ref cache) = self.cache
&& let Some(cached) = cache.get(&cache_key).await
@@ -284,7 +284,7 @@ impl PlcClient {
Ok(value)
}
pub async fn get_last_op(&self, did: &str) -> Result<PlcOpOrTombstone, PlcError> {
pub async fn get_last_op(&self, did: &Did) -> Result<PlcOpOrTombstone, PlcError> {
let url = format!("{}/{}/log/last", self.base_url, Self::encode_did(did));
let response = self.client.get(&url).send().await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
@@ -304,7 +304,7 @@ impl PlcClient {
.map_err(|e| PlcError::InvalidResponse(e.to_string()))
}
pub async fn get_audit_log(&self, did: &str) -> Result<Vec<Value>, PlcError> {
pub async fn get_audit_log(&self, did: &Did) -> Result<Vec<Value>, PlcError> {
let url = format!("{}/{}/log/audit", self.base_url, Self::encode_did(did));
let response = self.client.get(&url).send().await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
@@ -324,7 +324,7 @@ impl PlcClient {
.map_err(|e| PlcError::InvalidResponse(e.to_string()))
}
pub async fn send_operation(&self, did: &str, operation: &Value) -> Result<(), PlcError> {
pub async fn send_operation(&self, did: &Did, operation: &Value) -> Result<(), PlcError> {
let url = format!("{}/{}", self.base_url, Self::encode_did(did));
let response = self.client.post(&url).json(operation).send().await?;
if !response.status().is_success() {
@@ -512,7 +512,7 @@ pub fn validate_rotation_did_key(did_key: &str) -> Result<(), String> {
}
pub struct GenesisResult {
pub did: String,
pub did: Did,
pub signed_operation: Value,
}
@@ -553,7 +553,7 @@ pub fn create_genesis_operation(
})
}
pub fn did_for_genesis_op(signed_op: &Value) -> Result<String, PlcError> {
pub fn did_for_genesis_op(signed_op: &Value) -> Result<Did, PlcError> {
let cbor_bytes = serde_ipld_dagcbor::to_vec(signed_op)
.map_err(|e| PlcError::Serialization(e.to_string()))?;
let mut hasher = Sha256::new();
@@ -561,7 +561,7 @@ pub fn did_for_genesis_op(signed_op: &Value) -> Result<String, PlcError> {
let hash = hasher.finalize();
let encoded = base32::encode(Alphabet::Rfc4648Lower { padding: false }, &hash);
let truncated = &encoded[..24];
Ok(format!("did:plc:{}", truncated))
Ok(Did::from(format!("did:plc:{}", truncated)))
}
pub fn validate_plc_operation(op: &Value) -> Result<PlcOpType, PlcError> {
+8 -9
View File
@@ -124,23 +124,22 @@ impl CarVerifier {
&self,
did: &Did,
) -> Result<DidDocument<'static>, VerifyError> {
let did_str = did.as_str();
if did_str.starts_with("did:plc:") {
self.resolve_plc_did(did_str).await
} else if did_str.starts_with("did:web:") {
self.resolve_web_did(did_str).await
if did.is_plc() {
self.resolve_plc_did(did).await
} else if did.is_web() {
self.resolve_web_did(did).await
} else {
Err(VerifyError::DidResolutionFailed(format!(
"Unsupported DID method: {}",
did_str
did
)))
}
}
async fn resolve_plc_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> {
async fn resolve_plc_did(&self, did: &Did) -> Result<DidDocument<'static>, VerifyError> {
let plc_url = std::env::var("PLC_DIRECTORY_URL")
.unwrap_or_else(|_| tranquil_config::get().plc.directory_url.clone());
let url = format!("{}/{}", plc_url, urlencoding::encode(did));
let url = format!("{}/{}", plc_url, urlencoding::encode(did.as_str()));
let response = self
.http_client
.get(&url)
@@ -162,7 +161,7 @@ impl CarVerifier {
Ok(doc.into_static())
}
async fn resolve_web_did(&self, did: &str) -> Result<DidDocument<'static>, VerifyError> {
async fn resolve_web_did(&self, did: &Did) -> Result<DidDocument<'static>, VerifyError> {
let domain = did.strip_prefix("did:web:").ok_or_else(|| {
VerifyError::DidResolutionFailed("Invalid did:web format".to_string())
})?;
+6 -6
View File
@@ -14,8 +14,8 @@ fn test_commit_signing_produces_valid_signature() {
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
let rev = Tid::now(LimitedU32::MIN);
let did_typed = jacquard_common::types::string::Did::new(did).unwrap();
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None);
let did = jacquard_common::types::string::Did::new(did).unwrap();
let unsigned = Commit::new_unsigned(did, data_cid, rev, None);
let signed = unsigned.sign(&signing_key).unwrap();
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
@@ -38,8 +38,8 @@ fn test_commit_signing_with_prev() {
Cid::from_str("bafyreigxmvutyl3k5m4guzwxv3xf34gfxjlykgfdqkjmf32vwb5vcjxlui").unwrap();
let rev = Tid::now(LimitedU32::MIN);
let did_typed = jacquard_common::types::string::Did::new(did).unwrap();
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, Some(prev_cid));
let did = jacquard_common::types::string::Did::new(did).unwrap();
let unsigned = Commit::new_unsigned(did, data_cid, rev, Some(prev_cid));
let signed = unsigned.sign(&signing_key).unwrap();
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
@@ -58,8 +58,8 @@ fn test_unsigned_commit_has_5_fields() {
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
let rev = Tid::from_str("3masrxv55po22").unwrap();
let did_typed = jacquard_common::types::string::Did::new(did).unwrap();
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None);
let did = jacquard_common::types::string::Did::new(did).unwrap();
let unsigned = Commit::new_unsigned(did, data_cid, rev, None);
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap();
+13 -12
View File
@@ -40,8 +40,8 @@ fn create_unsigned_jwt(header: &Value, claims: &Value) -> String {
#[test]
fn test_signature_attacks() {
let key_bytes = generate_user_key();
let did = "did:plc:test";
let token = create_access_token(did, &key_bytes).expect("create token");
let did = Did::from("did:plc:whelk".to_string());
let token = create_access_token(&did, &key_bytes).expect("create token");
let parts: Vec<&str> = token.split('.').collect();
let forged_signature = URL_SAFE_NO_PAD.encode([0u8; 64]);
@@ -143,7 +143,7 @@ fn test_algorithm_substitution_attacks() {
#[test]
fn test_token_type_confusion() {
let key_bytes = generate_user_key();
let did = "did:plc:test";
let did = Did::from("did:plc:whelk".to_string());
let refresh_token = create_refresh_token(&did, &key_bytes).expect("create refresh token");
let result = verify_access_token(&refresh_token, &key_bytes);
@@ -156,7 +156,7 @@ fn test_token_type_confusion() {
.contains("Invalid token type")
);
let access_token = create_access_token(did, &key_bytes).expect("create access token");
let access_token = create_access_token(&did, &key_bytes).expect("create access token");
let result = verify_refresh_token(&access_token, &key_bytes);
assert!(result.is_err(), "Access token as refresh must be rejected");
assert!(
@@ -168,9 +168,9 @@ fn test_token_type_confusion() {
);
let service_token = create_service_token(
did,
"did:web:target",
Some("com.example.method"),
&did,
&Did::from("did:web:nel.pet".to_string()),
Some(&Nsid::from("cafe.oyster.method".to_string())),
&key_bytes,
)
.unwrap();
@@ -434,8 +434,8 @@ fn test_claim_validation() {
#[test]
fn test_did_and_jti_extraction() {
let key_bytes = generate_user_key();
let did = "did:plc:legitimate";
let token = create_access_token(did, &key_bytes).expect("create token");
let did = Did::from("did:plc:limpet".to_string());
let token = create_access_token(&did, &key_bytes).expect("create token");
assert_eq!(get_did_from_token(&token).unwrap(), did);
assert!(get_did_from_token("invalid").is_err());
@@ -459,7 +459,7 @@ fn test_did_and_jti_extraction() {
#[test]
fn test_header_injection_and_constant_time() {
let key_bytes = generate_user_key();
let did = "did:plc:test";
let did = Did::from("did:plc:whelk".to_string());
let header = json!({
"alg": "ES256K", "typ": TokenType::Access.as_str(),
@@ -474,7 +474,7 @@ fn test_header_injection_and_constant_time() {
verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok()
);
let valid_token = create_access_token(did, &key_bytes).expect("create token");
let valid_token = create_access_token(&did, &key_bytes).expect("create token");
let parts: Vec<&str> = valid_token.split('.').collect();
let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
almost_valid[0] ^= 1;
@@ -500,7 +500,8 @@ async fn test_server_rejects_invalid_tokens() {
let http_client = client();
let key_bytes = generate_user_key();
let forged_token = create_access_token("did:plc:fake-user", &key_bytes).unwrap();
let forged_token =
create_access_token(&Did::from("did:plc:lyna".to_string()), &key_bytes).unwrap();
let res = http_client
.get(format!("{}/xrpc/com.atproto.server.getSession", url))
.header("Authorization", format!("Bearer {}", forged_token))
@@ -176,9 +176,9 @@ fn test_permissions_repo_collection_wildcard_prefix() {
fn test_permissions_rpc_lxm_wildcard_prefix() {
let perms =
ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.actor.getProfile"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.actor.getProfile")));
}
#[test]
+3 -3
View File
@@ -48,7 +48,7 @@ async fn test_reserve_signing_key_with_did() {
assert!(signing_key.starts_with("did:key:z"));
let row = repos
.infra
.get_reserved_signing_key_full(signing_key)
.get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string()))
.await
.expect("db error")
.expect("Reserved key not found in database");
@@ -75,7 +75,7 @@ async fn test_reserve_signing_key_stores_private_key() {
let signing_key = body["signingKey"].as_str().unwrap();
let row = repos
.infra
.get_reserved_signing_key_full(signing_key)
.get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string()))
.await
.expect("db error")
.expect("Reserved key not found in database");
@@ -185,7 +185,7 @@ async fn test_create_account_with_reserved_signing_key() {
assert!(!access_jwt.is_empty());
let reserved = repos
.infra
.get_reserved_signing_key_full(signing_key)
.get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string()))
.await
.expect("db error")
.expect("Reserved key not found");
+1 -1
View File
@@ -1359,7 +1359,7 @@ async fn parity_signing_key_reservation() {
let f = ParityFixture::new().await;
let did = test_did("sigkey");
let expires = chrono::Utc::now() + chrono::Duration::hours(1);
let pub_key = format!("did:key:z6Mk{}", Uuid::new_v4().simple());
let pub_key = Did::from(format!("did:key:z6Mk{}", Uuid::new_v4().simple()));
let priv_bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
f.pg.infra
+4 -3
View File
@@ -220,7 +220,7 @@ async fn fetch_lexicon_via_atproto(nsid: &Nsid) -> Result<LexiconDoc, ScopeExpan
Ok(record.value)
}
async fn resolve_lexicon_did_authority(authority: &str) -> Result<String, ScopeExpansionError> {
async fn resolve_lexicon_did_authority(authority: &str) -> Result<Did, ScopeExpansionError> {
let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or_else(|e| {
tracing::warn!("falling back to default DNS resolvers: {}", e);
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())
@@ -239,7 +239,8 @@ async fn resolve_lexicon_did_authority(authority: &str) -> Result<String, ScopeE
.flat_map(|record| record.iter())
.find_map(|data| {
let txt = String::from_utf8_lossy(data);
txt.strip_prefix("did=").map(|did| did.to_string())
txt.strip_prefix("did=")
.and_then(|did| Did::new(did.trim()).ok())
})
.ok_or_else(|| {
ScopeExpansionError::DnsResolution(format!(
@@ -249,7 +250,7 @@ async fn resolve_lexicon_did_authority(authority: &str) -> Result<String, ScopeE
})
}
async fn resolve_did_to_pds(did: &str) -> Result<String, ScopeExpansionError> {
async fn resolve_did_to_pds(did: &Did) -> Result<String, ScopeExpansionError> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
+22 -22
View File
@@ -352,7 +352,7 @@ mod tests {
assert!(!perms.has_full_access());
assert!(!perms.allows_repo(RepoAction::Create, "app.bsky.feed.post"));
assert!(!perms.allows_blob("image/png"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
assert!(!perms.allows_account(AccountAttr::Email, AccountAction::Manage));
}
@@ -366,26 +366,26 @@ mod tests {
#[test]
fn test_transition_generic_without_chat_blocks_chat() {
let perms = ScopePermissions::from_scope_string(Some("transition:generic"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.listConvos"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.getMessages"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.listConvos")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.getMessages")));
}
#[test]
fn test_transition_generic_with_chat_allows_chat() {
let perms =
ScopePermissions::from_scope_string(Some("transition:generic transition:chat.bsky"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.listConvos"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.getMessages"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.listConvos")));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.getMessages")));
}
#[test]
fn test_transition_chat_only_allows_chat() {
let perms = ScopePermissions::from_scope_string(Some("transition:chat.bsky"));
assert!(!perms.allows_repo(RepoAction::Create, "app.bsky.feed.post"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "chat.bsky.convo.getMessages"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_repo(RepoAction::Create, &c("app.bsky.feed.post")));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("chat.bsky.convo.getMessages")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
}
#[test]
@@ -448,18 +448,18 @@ mod tests {
let perms = ScopePermissions::from_scope_string(Some(
"rpc:app.bsky.feed.getTimeline?aud=did:web:api.bsky.app",
));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed"));
assert!(!perms.allows_rpc("did:web:other.service", "app.bsky.feed.getTimeline"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
assert!(!perms.allows_rpc("did:web:other.service", &c("app.bsky.feed.getTimeline")));
}
#[test]
fn test_granular_rpc_wildcard_aud() {
let perms =
ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.getTimeline?aud=*"));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(perms.allows_rpc("did:web:any.service", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
assert!(perms.allows_rpc("did:web:any.service", &c("app.bsky.feed.getTimeline")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
}
#[test]
@@ -539,7 +539,7 @@ mod tests {
assert!(!perms.allows_repo(RepoAction::Delete, &c("cafe.oyster.record")));
assert!(!perms.allows_repo(RepoAction::Update, &c("cafe.oyster.record")));
assert!(perms.allows_blob("image/png"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
}
#[test]
@@ -550,7 +550,7 @@ mod tests {
assert!(!perms.allows_repo(RepoAction::Delete, &c("cafe.oyster.record")));
assert!(!perms.allows_repo(RepoAction::Update, &c("cafe.oyster.record")));
assert!(!perms.allows_blob("image/png"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
}
#[test]
@@ -558,7 +558,7 @@ mod tests {
let perms = ScopePermissions::from_scope_string(Some(
"rpc:app.bsky.feed.getAuthorFeed?aud=did:web:api.bsky.app#bsky_appview",
));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
assert!(perms.allows_rpc(
"did:web:api.bsky.app#bsky_appview",
"app.bsky.feed.getAuthorFeed"
@@ -567,8 +567,8 @@ mod tests {
"did:web:api.bsky.app#other_service",
"app.bsky.feed.getAuthorFeed"
));
assert!(!perms.allows_rpc("did:web:other.app", "app.bsky.feed.getAuthorFeed"));
assert!(!perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getTimeline"));
assert!(!perms.allows_rpc("did:web:other.app", &c("app.bsky.feed.getAuthorFeed")));
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
}
#[test]
@@ -576,7 +576,7 @@ mod tests {
let perms = ScopePermissions::from_scope_string(Some(
"rpc:app.bsky.feed.getAuthorFeed?aud=did:web:api.bsky.app",
));
assert!(perms.allows_rpc("did:web:api.bsky.app", "app.bsky.feed.getAuthorFeed"));
assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
assert!(perms.allows_rpc(
"did:web:api.bsky.app#bsky_appview",
"app.bsky.feed.getAuthorFeed"
+3 -3
View File
@@ -507,7 +507,7 @@ async fn bench_pg_upsert_records(
futures::stream::iter(user_ids.iter().zip(dids.iter()))
.fold((), |(), (uid, did)| async {
pg_create_user(pg, *uid, did).await;
let did_typed = Did::from(did.clone());
let did = Did::from(did.clone());
let handle = Handle::from(format!("bench.{}.invalid", uid.as_simple()));
repo.create_repo(
*uid,
@@ -612,7 +612,7 @@ async fn bench_pg_get_record_cid(pg: &sqlx::PgPool, concurrency: usize, ops_per_
let collection = Nsid::from("app.bsky.feed.post".to_string());
pg_create_user(pg, user_id, &did).await;
let repo = tranquil_db::postgres::PostgresRepoRepository::new(pg.clone());
let did_typed = Did::from(did.clone());
let did = Did::from(did);
let handle = Handle::from(format!("bench.{}.invalid", user_id.as_simple()));
repo.create_repo(
user_id,
@@ -668,7 +668,7 @@ async fn bench_pg_list_records(pg: &sqlx::PgPool, concurrency: usize, ops_per_ta
let collection = Nsid::from("app.bsky.feed.post".to_string());
pg_create_user(pg, user_id, &did).await;
let repo = tranquil_db::postgres::PostgresRepoRepository::new(pg.clone());
let did_typed = Did::from(did.clone());
let did = Did::from(did);
let handle = Handle::from(format!("bench.{}.invalid", user_id.as_simple()));
repo.create_repo(
user_id,
@@ -268,7 +268,7 @@ mod tests {
let ops = h.metastore.backlink_ops();
let (_user_id, user_hash) = create_repo(&h, "olaren", 1);
let uri = AtUri::from_parts("did:plc:olaren", "app.bsky.feed.like", "3k2abc");
let uri = test_uri("did:plc:olaren", "app.bsky.feed.like", "3k2abc");
let backlinks = vec![Backlink {
uri: uri.clone(),
path: BacklinkPath::SubjectUri,
@@ -295,7 +295,7 @@ mod tests {
let ops = h.metastore.backlink_ops();
let (_user_id, user_hash) = create_repo(&h, "teq", 2);
let uri = AtUri::from_parts("did:plc:teq", "app.bsky.graph.follow", "3k2fol");
let uri = test_uri("did:plc:teq", "app.bsky.graph.follow", "3k2fol");
let backlinks = vec![Backlink {
uri: uri.clone(),
path: BacklinkPath::Subject,
@@ -342,7 +342,7 @@ mod tests {
let backlinks: Vec<Backlink> = (0..5)
.map(|i| Backlink {
uri: AtUri::from_parts("did:plc:nel", "app.bsky.feed.like", &format!("3k2r{i}")),
uri: test_uri("did:plc:nel", "app.bsky.feed.like", &format!("3k2r{i}")),
path: BacklinkPath::SubjectUri,
link_to: format!("at://did:plc:target{i}/app.bsky.feed.post/3k2p{i}"),
})
@@ -382,7 +382,7 @@ mod tests {
let (user_id, user_hash) = create_repo(&h, "lyna", 4);
let existing = Backlink {
uri: AtUri::from_parts("did:plc:lyna", "app.bsky.feed.like", "3k2old"),
uri: test_uri("did:plc:lyna", "app.bsky.feed.like", "3k2old"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
};
@@ -393,7 +393,7 @@ mod tests {
batch.commit().unwrap();
let proposed = vec![Backlink {
uri: AtUri::from_parts("did:plc:lyna", "app.bsky.feed.like", "3k2new"),
uri: test_uri("did:plc:lyna", "app.bsky.feed.like", "3k2new"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
}];
@@ -417,7 +417,7 @@ mod tests {
let (user_id, user_hash) = create_repo(&h, "bailey", 5);
let existing = Backlink {
uri: AtUri::from_parts("did:plc:bailey", "app.bsky.feed.like", "3k2old"),
uri: test_uri("did:plc:bailey", "app.bsky.feed.like", "3k2old"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
};
@@ -428,7 +428,7 @@ mod tests {
batch.commit().unwrap();
let proposed = vec![Backlink {
uri: AtUri::from_parts("did:plc:bailey", "app.bsky.feed.repost", "3k2new"),
uri: test_uri("did:plc:bailey", "app.bsky.feed.repost", "3k2new"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
}];
@@ -448,7 +448,7 @@ mod tests {
let (user_id, user_hash) = create_repo(&h, "olaren", 6);
let existing = Backlink {
uri: AtUri::from_parts("did:plc:olaren", "app.bsky.graph.follow", "3k2old"),
uri: test_uri("did:plc:olaren", "app.bsky.graph.follow", "3k2old"),
path: BacklinkPath::Subject,
link_to: "did:plc:target".to_string(),
};
@@ -459,7 +459,7 @@ mod tests {
batch.commit().unwrap();
let proposed = vec![Backlink {
uri: AtUri::from_parts("did:plc:olaren", "app.bsky.graph.follow", "3k2new"),
uri: test_uri("did:plc:olaren", "app.bsky.graph.follow", "3k2new"),
path: BacklinkPath::SubjectUri,
link_to: "did:plc:target".to_string(),
}];
@@ -480,7 +480,7 @@ mod tests {
let (user_id_b, _user_hash_b) = create_repo(&h, "nel", 8);
let existing = Backlink {
uri: AtUri::from_parts("did:plc:teq", "app.bsky.feed.like", "3k2old"),
uri: test_uri("did:plc:teq", "app.bsky.feed.like", "3k2old"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:target/app.bsky.feed.post/3k2p1".to_string(),
};
@@ -491,7 +491,7 @@ mod tests {
batch.commit().unwrap();
let proposed = vec![Backlink {
uri: AtUri::from_parts("did:plc:nel", "app.bsky.feed.like", "3k2new"),
uri: test_uri("did:plc:nel", "app.bsky.feed.like", "3k2new"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:target/app.bsky.feed.post/3k2p1".to_string(),
}];
@@ -511,7 +511,7 @@ mod tests {
let (user_id, user_hash) = create_repo(&h, "lyna", 12);
let existing = Backlink {
uri: AtUri::from_parts("did:plc:lyna", "app.bsky.feed.like", "3k2same"),
uri: test_uri("did:plc:lyna", "app.bsky.feed.like", "3k2same"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
};
@@ -522,7 +522,7 @@ mod tests {
batch.commit().unwrap();
let proposed = vec![Backlink {
uri: AtUri::from_parts("did:plc:lyna", "app.bsky.feed.like", "3k2same"),
uri: test_uri("did:plc:lyna", "app.bsky.feed.like", "3k2same"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
}];
@@ -555,12 +555,12 @@ mod tests {
let (_user_id, user_hash) = create_repo(&h, "bailey", 10);
let bl1 = Backlink {
uri: AtUri::from_parts("did:plc:bailey", "app.bsky.feed.like", "3k2aaa"),
uri: test_uri("did:plc:bailey", "app.bsky.feed.like", "3k2aaa"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:t1/app.bsky.feed.post/p1".to_string(),
};
let bl2 = Backlink {
uri: AtUri::from_parts("did:plc:bailey", "app.bsky.feed.like", "3k2bbb"),
uri: test_uri("did:plc:bailey", "app.bsky.feed.like", "3k2bbb"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:t2/app.bsky.feed.post/p2".to_string(),
};
@@ -608,7 +608,7 @@ mod tests {
let (user_id, user_hash) = create_repo(&h, "kate", 11);
let existing = Backlink {
uri: AtUri::from_parts("did:plc:kate", "app.bsky.feed.like", "3k2old"),
uri: test_uri("did:plc:kate", "app.bsky.feed.like", "3k2old"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
};
@@ -620,12 +620,12 @@ mod tests {
let proposed = vec![
Backlink {
uri: AtUri::from_parts("did:plc:kate", "app.bsky.feed.like", "3k2new1"),
uri: test_uri("did:plc:kate", "app.bsky.feed.like", "3k2new1"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
},
Backlink {
uri: AtUri::from_parts("did:plc:kate", "app.bsky.feed.like", "3k2new2"),
uri: test_uri("did:plc:kate", "app.bsky.feed.like", "3k2new2"),
path: BacklinkPath::SubjectUri,
link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(),
},
+12 -5
View File
@@ -1726,7 +1726,10 @@ impl<S: StorageIO + 'static> tranquil_db_traits::SessionRepository for Metastore
recv(rx).await
}
async fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, DbError> {
async fn get_app_password_hashes_by_did(
&self,
did: &Did,
) -> Result<Vec<PasswordHash>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool.send(MetastoreRequest::Session(
SessionRequest::GetAppPasswordHashesByDid {
@@ -2041,7 +2044,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn reserve_signing_key(
&self,
did: Option<&Did>,
public_key_did_key: &str,
public_key_did_key: &Did,
private_key_bytes: &[u8],
expires_at: DateTime<Utc>,
) -> Result<Uuid, DbError> {
@@ -2059,7 +2062,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn get_reserved_signing_key(
&self,
public_key_did_key: &str,
public_key_did_key: &Did,
) -> Result<Option<ReservedSigningKey>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool.send(MetastoreRequest::Infra(
@@ -2466,7 +2469,7 @@ impl<S: StorageIO + 'static> tranquil_db_traits::InfraRepository for MetastoreCl
async fn get_reserved_signing_key_full(
&self,
public_key_did_key: &str,
public_key_did_key: &Did,
) -> Result<Option<ReservedSigningKeyFull>, DbError> {
let (tx, rx) = oneshot::channel();
self.pool.send(MetastoreRequest::Infra(
@@ -3730,7 +3733,11 @@ impl<S: StorageIO + 'static> tranquil_db_traits::UserRepository for MetastoreCli
recv(rx).await
}
async fn admin_update_password(&self, did: &Did, password_hash: &str) -> Result<u64, DbError> {
async fn admin_update_password(
&self,
did: &Did,
password_hash: &PasswordHash,
) -> Result<u64, DbError> {
let (tx, rx) = oneshot::channel();
self.pool
.send(MetastoreRequest::User(UserRequest::AdminUpdatePassword {
+14 -16
View File
@@ -253,7 +253,7 @@ pub enum RepoRequest {
impl RepoRequest {
fn routing(&self, user_hashes: &UserHashMap) -> Routing {
match self {
Self::CreateRepoFull { did, .. } => did_to_routing(did.as_str()),
Self::CreateRepoFull { did, .. } => did_to_routing(did),
Self::UpdateRepoRoot { user_id, .. }
| Self::UpdateRepoRev { user_id, .. }
| Self::DeleteRepo { user_id, .. }
@@ -262,7 +262,7 @@ impl RepoRequest {
| Self::GetRepoRootCidByUserId { user_id, .. } => uuid_to_routing(user_hashes, user_id),
Self::GetRepoRootByDid { did, .. }
| Self::GetAccountWithRepo { did, .. }
| Self::UpdateRepoStatus { did, .. } => did_to_routing(did.as_str()),
| Self::UpdateRepoStatus { did, .. } => did_to_routing(did),
Self::CountRepos { .. }
| Self::GetReposWithoutRev { .. }
| Self::ListReposPaginated { .. } => Routing::Global,
@@ -503,7 +503,7 @@ pub enum CommitRequest {
impl CommitRequest {
fn routing(&self, user_hashes: &UserHashMap) -> Routing {
match self {
Self::ApplyCommit { input, .. } => did_to_routing(input.did.as_str()),
Self::ApplyCommit { input, .. } => did_to_routing(&input.did),
Self::ImportRepoData { user_id, .. }
| Self::InsertRecordBlobs {
repo_id: user_id, ..
@@ -644,7 +644,7 @@ impl BlobRequest {
Self::ListMissingBlobs { repo_id, .. }
| Self::CountDistinctRecordBlobs { repo_id, .. }
| Self::GetBlobsForExport { repo_id, .. } => uuid_to_routing(user_hashes, repo_id),
Self::ListBlobsSinceRev { did, .. } => did_to_routing(did.as_str()),
Self::ListBlobsSinceRev { did, .. } => did_to_routing(did),
}
}
}
@@ -731,7 +731,7 @@ impl DelegationRequest {
}
| Self::CountAuditLogEntries {
delegated_did: did, ..
} => did_to_routing(did.as_str()),
} => did_to_routing(did),
Self::CreateDelegation { delegated_did, .. }
| Self::RevokeDelegation { delegated_did, .. }
| Self::UpdateDelegationScopes { delegated_did, .. }
@@ -743,7 +743,7 @@ impl DelegationRequest {
| Self::ControlsAnyAccounts {
did: controller_did,
..
} => did_to_routing(controller_did.as_str()),
} => did_to_routing(controller_did),
}
}
}
@@ -822,7 +822,7 @@ impl SsoRequest {
match self {
Self::CreateExternalIdentity { did, .. }
| Self::GetExternalIdentitiesByDid { did, .. }
| Self::DeleteExternalIdentity { did, .. } => did_to_routing(did.as_str()),
| Self::DeleteExternalIdentity { did, .. } => did_to_routing(did),
Self::GetExternalIdentityByProvider { .. }
| Self::UpdateExternalIdentityLogin { .. }
| Self::ConsumeSsoAuthState { .. }
@@ -964,7 +964,7 @@ impl SessionRequest {
| Self::UpdateLastReauth { did, .. }
| Self::GetSessionMfaStatus { did, .. }
| Self::UpdateMfaVerified { did, .. }
| Self::GetAppPasswordHashesByDid { did, .. } => did_to_routing(did.as_str()),
| Self::GetAppPasswordHashesByDid { did, .. } => did_to_routing(did),
Self::RefreshSessionAtomic { data, .. } => did_to_routing(data.did.as_str()),
Self::ListAppPasswords { user_id, .. }
| Self::GetAppPasswordsForLogin { user_id, .. }
@@ -1721,7 +1721,7 @@ impl UserRequest {
}
| Self::SetRecoveryToken { did, .. }
| Self::EnableTotpVerified { did, .. }
| Self::SetTwoFactorEnabled { did, .. } => did_to_routing(did.as_str()),
| Self::SetTwoFactorEnabled { did, .. } => did_to_routing(did),
Self::GetCommsPrefs { user_id, .. }
| Self::GetUserKeyById { user_id, .. }
@@ -2101,7 +2101,7 @@ impl InfraRequest {
| Self::GetAdminAccountInfoByDid { did, .. }
| Self::GetDeletionRequestByDid { did, .. }
| Self::GetPlcTokensByDid { did, .. }
| Self::CountPlcTokensByDid { did, .. } => did_to_routing(did.as_str()),
| Self::CountPlcTokensByDid { did, .. } => did_to_routing(did),
Self::GetBlobStorageKeyByCid { cid, .. } | Self::DeleteBlobByCid { cid, .. } => {
cid_to_routing(cid)
}
@@ -2405,14 +2405,12 @@ impl OAuthRequest {
| Self::ListSessionsByDid { did, .. }
| Self::DeleteSessionsByDid { did, .. }
| Self::DeleteSessionsByDidExcept { did, .. }
| Self::DeleteSessionById { did, .. } => did_to_routing(did.as_str()),
Self::RevokeTokensForController { delegated_did, .. } => {
did_to_routing(delegated_did.as_str())
}
| Self::DeleteSessionById { did, .. } => did_to_routing(did),
Self::RevokeTokensForController { delegated_did, .. } => did_to_routing(delegated_did),
Self::SetAuthorizationDid { did, .. }
| Self::UpdateAuthorizationRequest { did, .. }
| Self::MarkRequestAuthenticated { did, .. }
| Self::SetRequestDid { did, .. } => did_to_routing(did.as_str()),
| Self::SetRequestDid { did, .. } => did_to_routing(did),
Self::CreateToken { .. }
| Self::GetTokenById { .. }
| Self::GetTokenByRefreshToken { .. }
@@ -6284,7 +6282,7 @@ mod tests {
.unwrap();
let user_hashes = ms.user_hashes().as_ref();
let did = Did::from("did:plc:limpet".to_string());
let expected = did_to_routing(did.as_str());
let expected = did_to_routing(&did);
let sid = SessionId::new(7);
let (tx, _rx) = oneshot::channel();
@@ -1461,7 +1461,7 @@ impl InfraOps {
Ok(val.map(|v| ReservedSigningKeyFull {
id: v.id,
did: v.did.and_then(|d| Did::new(d).ok()),
public_key_did_key: v.public_key_did_key,
public_key_did_key: Did::from(v.public_key_did_key),
private_key_bytes: v.private_key_bytes,
expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(),
used_at: v.used_at_ms.and_then(DateTime::from_timestamp_millis),