Skip to main content

alibi_api/plugins/siwe/
config.rs

1use alibi_core::AuthResponse;
2use async_trait::async_trait;
3use rand::RngExt;
4use rand::distr::Alphanumeric;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::sync::Arc;
8
9/// An application callback may reject with an endpoint response or fail while contacting a wallet,
10/// identity, or nonce provider.
11///
12/// SIWE preserves endpoint responses and translates provider failures to its documented HTTP
13/// error.
14#[derive(Debug)]
15pub enum SiweCallbackError {
16    Api(AuthResponse),
17    Failed(String),
18    Unknown,
19}
20
21impl fmt::Display for SiweCallbackError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::Api(response) => {
25                write!(f, "SIWE callback rejected with HTTP {}", response.status)
26            }
27            Self::Failed(message) => f.write_str(message),
28            Self::Unknown => f.write_str("Unknown error"),
29        }
30    }
31}
32
33impl std::error::Error for SiweCallbackError {}
34
35pub type SiweCallbackResult<T> = Result<T, SiweCallbackError>;
36
37#[async_trait]
38pub trait SiweNonceProvider: Send + Sync {
39    /// Return an ERC-4361 nonce containing 8–250 ASCII alphanumeric characters.
40    async fn get_nonce(&self) -> SiweCallbackResult<String>;
41}
42
43/// A cryptographically random nonce provider suitable for ordinary deployments.
44#[derive(Debug, Clone, Copy, Default)]
45pub struct RandomSiweNonce;
46
47#[async_trait]
48impl SiweNonceProvider for RandomSiweNonce {
49    async fn get_nonce(&self) -> SiweCallbackResult<String> {
50        Ok(rand::rng()
51            .sample_iter(&Alphanumeric)
52            .take(32)
53            .map(char::from)
54            .collect())
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct CacaoHeader {
60    pub t: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct CacaoPayload {
65    pub domain: String,
66    pub aud: String,
67    pub nonce: String,
68    pub iss: String,
69    pub version: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct CacaoSignature {
74    pub t: String,
75    pub s: String,
76}
77
78/// CAIP-122 callback data constructed by Better Auth 1.7.6. Its domain, audience
79/// and issuer retain the configured domain rather than the normalized host.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct Cacao {
82    pub h: CacaoHeader,
83    pub p: CacaoPayload,
84    pub s: CacaoSignature,
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct SiweVerification {
89    pub message: String,
90    pub signature: String,
91    /// EIP-55 checksummed address from the signed message.
92    pub address: String,
93    /// The positive integer JavaScript Number parsed from the signed message.
94    pub chain_id: f64,
95    pub cacao: Cacao,
96}
97
98#[async_trait]
99pub trait SiweVerifier: Send + Sync {
100    /// Verify the original signed bytes for the supplied address and chain.
101    /// Implementations may support externally owned or ERC-1271 contract wallets.
102    async fn verify_message(&self, input: SiweVerification) -> SiweCallbackResult<bool>;
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct EnsProfile {
107    pub name: Option<String>,
108    pub avatar: Option<String>,
109}
110
111#[async_trait]
112pub trait EnsLookup: Send + Sync {
113    async fn lookup(&self, wallet_address: &str) -> SiweCallbackResult<EnsProfile>;
114}
115
116/// SIWE application policies. A verifier is required because contract-wallet
117/// verification and chain-specific RPC access are owned by the application.
118#[derive(Clone)]
119pub struct SiweConfig {
120    pub domain: String,
121    pub nonce_provider: Arc<dyn SiweNonceProvider>,
122    pub verifier: Arc<dyn SiweVerifier>,
123    pub email_domain_name: Option<String>,
124    /// When false, an email is required. Existing email identities are never
125    /// linked to a wallet using that email alone.
126    pub anonymous: bool,
127    pub ens_lookup: Option<Arc<dyn EnsLookup>>,
128}
129
130impl SiweConfig {
131    #[must_use]
132    pub fn new(
133        domain: impl Into<String>,
134        nonce_provider: Arc<dyn SiweNonceProvider>,
135        verifier: Arc<dyn SiweVerifier>,
136    ) -> Self {
137        Self {
138            domain: domain.into(),
139            nonce_provider,
140            verifier,
141            email_domain_name: None,
142            anonymous: true,
143            ens_lookup: None,
144        }
145    }
146}
147
148impl fmt::Debug for SiweConfig {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("SiweConfig")
151            .field("domain", &self.domain)
152            .field("email_domain_name", &self.email_domain_name)
153            .field("anonymous", &self.anonymous)
154            .field("has_ens_lookup", &self.ens_lookup.is_some())
155            .finish_non_exhaustive()
156    }
157}