mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-07-29 08:12:37 +00:00
db: Did type for repos and handlers
Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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())
|
||||
})?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user