Skip to content

Conversation

@yaslama
Copy link
Member

@yaslama yaslama commented Nov 13, 2025

No description provided.

@yaslama yaslama requested review from JssDWt and Copilot November 13, 2025 12:50
Copilot finished reviewing on behalf of yaslama November 13, 2025 12:53
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a flexible domain validation system using Fly.io certificates, refactoring the hardcoded domain list approach to a trait-based validator pattern. The implementation enables dynamic domain validation through Fly.io's certificate API as an alternative to static configuration.

Key Changes

  • Created domain-validator crate with a DomainValidator trait and ListDomainValidator implementation for backwards compatibility with static domain lists
  • Created fly-api crate with FlyDomainValidator that queries Fly.io's GraphQL API to validate domains against app certificates
  • Refactored lnurl crate to use the DomainValidator trait, replacing direct domain set checks and removing the sanitize_domain helper function

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
crates/domain-validator/Cargo.toml Defines the new domain-validator crate with trait definition and list-based implementation
crates/domain-validator/src/lib.rs Implements the DomainValidator trait and ListDomainValidator for static domain lists
crates/fly-api/Cargo.toml Defines the new fly-api crate for Fly.io certificate-based validation
crates/fly-api/src/lib.rs Implements FlyDomainValidator that queries Fly.io GraphQL API for certificate domains
crates/breez-sdk/lnurl/Cargo.toml Adds dependencies on domain-validator and fly-api crates
crates/breez-sdk/lnurl/src/state.rs Replaces domains HashSet with Arc trait object
crates/breez-sdk/lnurl/src/main.rs Adds CLI args for Fly.io configuration and instantiates appropriate validator
crates/breez-sdk/lnurl/src/routes.rs Updates all route handlers to use domain validator trait, removes sanitize_domain function

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +18
pub struct FlyDomainValidator {
app_name: String,
api_token: String,
client: Client,
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FlyDomainValidator struct lacks documentation. Add doc comments explaining its purpose (validates domains against Fly.io app certificates), the required parameters (app_name and api_token), and usage example. This is a public API that will be used by consumers of this crate.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +103
let allowed_domains = self
.get_certificate_domains()
.await
.map_err(|e| DomainValidatorError::DomainNotAllowed(e.to_string()))?;
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_certificate_domains() method is called on every domain validation, which makes a network request to the Fly.io API each time. This creates an N+1 query pattern for multiple domain validations and can significantly impact performance. Consider implementing caching (e.g., with a TTL) to avoid repeated API calls for the same certificate data.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +17
pub struct FlyDomainValidator {
app_name: String,
api_token: String,
client: Client,
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The api_token field is stored as a plain String. Consider using a secure type like secrecy::Secret<String> or similar to prevent accidental logging or exposure of the API token through debug output. The Debug trait on the struct will currently expose the token.

Copilot uses AI. Check for mistakes.

if allowed_domains.is_empty() {
return Err(FlyApiError::FetchError(
"No domains found in Fly.io certificates".to_string(),
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "No domains found in Fly.io certificates" is unclear about the cause. This could happen for multiple reasons: no certificates exist, certificates have no hostnames, or the GraphQL response structure is unexpected. Consider a more specific message like "No valid certificates with hostnames found for app '{app_name}'" or include troubleshooting hints.

Suggested change
"No domains found in Fly.io certificates".to_string(),
format!(
"No valid certificates with hostnames found for app '{}'. \
Possible causes: no certificates exist, certificates have no hostnames, or the app name is incorrect. \
Please check your Fly.io app configuration and certificate status.",
self.app_name
),

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +12
#[async_trait::async_trait]
pub trait DomainValidator: Send + Sync {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DomainValidator trait lacks documentation. Add doc comments explaining its purpose, the expected behavior of validate_domain(), and when DomainNotAllowed error should be returned. This is a public trait that will be implemented by external consumers.

Suggested change
#[async_trait::async_trait]
pub trait DomainValidator: Send + Sync {
#[async_trait::async_trait]
/// A trait for validating whether a domain is allowed.
///
/// This trait should be implemented by external consumers to provide custom domain validation logic.
/// The `validate_domain` method checks if the given domain is permitted according to the implementation's rules.
/// If the domain is not allowed, it should return `Err(DomainValidatorError::DomainNotAllowed(domain.to_string()))`.
/// Otherwise, it should return `Ok(())`.
pub trait DomainValidator: Send + Sync {
/// Asynchronously validates whether the provided domain is allowed.
///
/// # Arguments
///
/// * `domain` - The domain name to validate.
///
/// # Returns
///
/// * `Ok(())` if the domain is allowed.
/// * `Err(DomainValidatorError::DomainNotAllowed)` if the domain is not permitted.
///
/// Implementors should return `DomainNotAllowed` only when the domain fails validation according to their rules.

Copilot uses AI. Check for mistakes.
}

let username = sanitize_username(&identifier);
let domain = host.trim().to_lowercase();
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request_invoice function does not validate the domain before creating invoices. Unlike the register function (lines 134-137), this allows invoice generation for unauthorized domains. Consider adding domain validation here to ensure only allowed domains can request invoices.

Suggested change
let domain = host.trim().to_lowercase();
let domain = host.trim().to_lowercase();
// Domain validation: ensure only allowed domains can request invoices
if let Some(allowed_domains) = &state.allowed_domains {
if !allowed_domains.contains(&domain) {
return Err((
StatusCode::FORBIDDEN,
Json(Value::String("domain not allowed".into())),
));
}
}

Copilot uses AI. Check for mistakes.
@yaslama yaslama requested a review from Copilot November 13, 2025 13:31
Copilot finished reviewing on behalf of yaslama November 13, 2025 13:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

max_sendable: args.max_sendable,
include_spark_address: args.include_spark_address,
domains,
domain_validator: domain_validator,
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant use of variable name in field initialization. This can be simplified from domain_validator: domain_validator, to just domain_validator, using Rust's field init shorthand.

Suggested change
domain_validator: domain_validator,
domain_validator,

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +90
return Err(FlyApiError::FetchError(
"No domains found in Fly.io certificates".to_string(),
));
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "No domains found in Fly.io certificates" doesn't distinguish between an empty response and a malformed response. This could be misleading if the app exists but has no certificates configured. Consider making the error message more specific, such as "No certificates configured for the Fly.io app" or checking for the app's existence first.

Copilot uses AI. Check for mistakes.
@yaslama yaslama requested a review from Copilot November 13, 2025 13:42
Copilot finished reviewing on behalf of yaslama November 13, 2025 13:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +202 to +205
let domain = host.trim().to_lowercase();
let user = state
.db
.get_user_by_pubkey(&sanitize_domain(&state, &host)?, &pubkey.to_string())
.get_user_by_pubkey(&domain, &pubkey.to_string())
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain validation is missing in the recover_lnurl_pay endpoint. The old sanitize_domain function validated the domain before allowing recovery operations, but the new implementation directly uses the domain without validation. This could allow unauthorized operations on invalid domains. Consider adding domain validation similar to the register_lnurl_pay endpoint.

Copilot uses AI. Check for mistakes.
Comment on lines +242 to +245
let domain = host.trim().to_lowercase();
let user = state
.db
.get_user_by_name(&sanitize_domain(&state, &host)?, &username)
.get_user_by_name(&domain, &username)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain validation is missing in the get_lnurl_pay endpoint. The old sanitize_domain function validated the domain, but the new implementation directly uses the domain without validation. This could expose data for invalid domains. Consider adding domain validation similar to the register_lnurl_pay endpoint.

Copilot uses AI. Check for mistakes.
Comment on lines +287 to 290
let domain = host.trim().to_lowercase();
let user = state
.db
.get_user_by_name(&domain, &username)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain validation is missing in the get_lnurl_pay_callback endpoint. The old sanitize_domain function validated the domain, but the new implementation directly uses the domain without validation. This could allow unauthorized payment callbacks for invalid domains. Consider adding domain validation similar to the register_lnurl_pay endpoint.

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +181
let domain = host.trim().to_lowercase();
state
.db
.delete_user(&sanitize_domain(&state, &host)?, &pubkey.to_string())
.delete_user(&domain, &pubkey.to_string())
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain validation is missing in the delete_lnurl_pay endpoint. The old sanitize_domain function validated the domain before allowing deletion, but the new implementation directly uses the domain without validation. This could allow unauthorized deletions on invalid domains. Consider adding domain validation similar to the register_lnurl_pay endpoint.

Copilot uses AI. Check for mistakes.
@yaslama yaslama requested a review from Copilot November 13, 2025 15:50
Copilot finished reviewing on behalf of yaslama November 13, 2025 15:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 11 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +24
pub fn new(app_name: String, api_token: String) -> Self {
Self {
app_name,
api_token,
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor accepts String parameters which are moved into the struct. Consider accepting impl Into<String> or &str parameters to provide a more flexible API that doesn't require callers to own the strings.

Suggested change
pub fn new(app_name: String, api_token: String) -> Self {
Self {
app_name,
api_token,
pub fn new(app_name: impl Into<String>, api_token: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
api_token: api_token.into(),

Copilot uses AI. Check for mistakes.
pub session_manager: Arc<InMemorySessionManager>,
pub service_provider: Arc<ServiceProvider>,
pub subscribed_keys: Arc<Mutex<HashSet<String>>>,
pub subscribed_keys: Arc<Mutex<std::collections::HashSet<String>>>,
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of std::collections::HashSet here is inconsistent with the rest of the file where HashSet was previously imported. Since std::collections::HashSet was removed from imports (line 6), this should use the fully qualified path consistently or add back the import. The inconsistency makes the code harder to maintain.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
pub trait DomainValidator: Send + Sync {
async fn validate_domain(&self, domain: &str) -> Result<(), DomainValidatorError>;
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DomainValidator trait lacks documentation explaining its purpose, expected behavior, and usage. Add doc comments to describe what domain validation means in this context and how implementers should handle the validation.

Copilot uses AI. Check for mistakes.
Extension(state): Extension<State<DB>>,
) -> Result<Json<CheckUsernameAvailableResponse>, (StatusCode, Json<Value>)> {
let username = sanitize_username(&identifier);
let domain = host.trim().to_lowercase();
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain sanitization logic (host.trim().to_lowercase()) is duplicated across multiple route handlers (lines 98, 131, 178, 202, 242, 287). Consider extracting this into a helper function to improve maintainability and consistency.

Copilot uses AI. Check for mistakes.
let response = self
.client
.post("https://api.fly.io/graphql")
.header("Authorization", format!("Bearer {}", self.api_token))
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API token is included in the Authorization header without any form of sanitization or validation. Consider validating the token format in the constructor to fail fast if an invalid token is provided, preventing potential security issues or misleading error messages.

Copilot uses AI. Check for mistakes.
pub trait DomainValidator: Send + Sync {
async fn validate_domain(&self, domain: &str) -> Result<(), DomainValidatorError>;
}

Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ListDomainValidator struct lacks documentation explaining its purpose and usage. Add doc comments to describe that this is a simple implementation that validates against a predefined list of allowed domains.

Suggested change
/// A simple implementation of the `DomainValidator` trait that validates domains
/// against a predefined list of allowed domains.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@JssDWt JssDWt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. It certainly saves quite some effort adding domains to this service. I added one comment for performance, and there's the failing CI, but otherwise LGTM.

Comment on lines +100 to +103
let allowed_domains = self
.get_certificate_domains()
.await
.map_err(|e| DomainValidatorError::DomainNotAllowed(e.to_string()))?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be good enough to load these certificates at startup. You can always restart the service after adding a certificate. It saves an api call per request.

Copy link
Member

@roeierez roeierez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only structural comments, otherwise LGTM

@@ -0,0 +1,21 @@
[package]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to move the domain-validator and fly-api crates under the breez sdk folder where lnurl crate is located.
Also would consider moving the domain-validator content into lnurl-models crate and get rid of the domain-validtor crate (I am not a fan of one trait crate).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants