alibi-core 0.11.0

Shared authentication contracts and backend-independent services for Alibi
Documentation
use crate::verification::{VerificationCreation, VerificationPublication, VerificationSnapshot};
use crate::{AuthError, AuthResult, AuthSchema, CreateVerification};
use async_trait::async_trait;
#[async_trait]
pub trait VerificationStore<S: AuthSchema>: Send + Sync {
    /// Create through before hooks, optional physical persistence, secondary
    /// publication, then after hooks. Unsupported adapters must fail closed.
    async fn create_verification_record(
        &self,
        _data: VerificationCreation,
        _publication: VerificationPublication,
    ) -> AuthResult<Option<VerificationSnapshot>> {
        Err(AuthError::NotImplemented(
            "Verification publication phases are not supported by this store".into(),
        ))
    }
    /// Atomically remove the newest generation and all siblings, returning the
    /// actual winning snapshot even when expired. This keeps legacy fallback
    /// from resurrecting an older logical generation.
    async fn consume_verification_snapshot(
        &self,
        _identifier: &str,
    ) -> AuthResult<Option<S::Verification>> {
        Err(AuthError::NotImplemented(
            "Raw atomic verification consumption is not supported by this store".into(),
        ))
    }
    /// Update every physical match, with one actual adapter result snapshot.
    async fn update_verification_by_identifier(
        &self,
        _identifier: &str,
        _data: crate::UpdateVerification,
    ) -> AuthResult<Option<VerificationSnapshot>> {
        Err(AuthError::NotImplemented(
            "Verification identifier updates are not supported by this store".into(),
        ))
    }
    /// Reserve by the original logical identifier while storing its configured
    /// transformed identifier. Return the actual newly inserted model only.
    async fn reserve_verification_record(
        &self,
        _logical_identifier: &str,
        _data: CreateVerification,
    ) -> AuthResult<Option<S::Verification>> {
        Err(AuthError::NotImplemented(
            "Configured verification reservation is not supported by this store".into(),
        ))
    }
    async fn create_verification(
        &self,
        verification: CreateVerification,
    ) -> AuthResult<S::Verification>;
    async fn get_verification(
        &self,
        identifier: &str,
        value: &str,
    ) -> AuthResult<Option<S::Verification>>;
    async fn get_verification_by_value(&self, value: &str) -> AuthResult<Option<S::Verification>>;
    async fn get_verification_by_identifier(
        &self,
        identifier: &str,
    ) -> AuthResult<Option<S::Verification>>;
    async fn consume_verification(
        &self,
        identifier: &str,
        value: &str,
    ) -> AuthResult<Option<S::Verification>>;
    /// Fetch the newest generation, including expired records. The caller
    /// decides whether cleanup and a distinct expiry error are required.
    async fn get_latest_verification_by_identifier(
        &self,
        _identifier: &str,
    ) -> AuthResult<Option<S::Verification>> {
        Err(AuthError::internal(
            "raw verification lookup is not supported by this store",
        ))
    }
    /// Atomically invalidate an identifier and return its newest generation.
    /// Exactly one concurrent caller can receive a row. Expired records are
    /// removed along with every sibling and return `None`.
    async fn consume_verification_by_identifier(
        &self,
        _identifier: &str,
    ) -> AuthResult<Option<S::Verification>> {
        Err(AuthError::internal(
            "atomic verification consumption is not supported by this store",
        ))
    }
    async fn delete_verifications_by_identifier(&self, _identifier: &str) -> AuthResult<()> {
        Err(AuthError::internal(
            "verification invalidation is not supported by this store",
        ))
    }
    /// Update a generation only when its value still matches the snapshot.
    async fn compare_and_swap_verification(
        &self,
        _id: &str,
        _expected_value: &str,
        _value: &str,
        _expires_at: chrono::DateTime<chrono::Utc>,
    ) -> AuthResult<bool> {
        Err(AuthError::internal(
            "atomic verification updates are not supported by this store",
        ))
    }
    /// Insert a deterministic reservation exactly once. An expired marker
    /// remains reserved until it is cleaned up or explicitly consumed.
    async fn reserve_verification(&self, _verification: CreateVerification) -> AuthResult<bool> {
        Err(AuthError::internal(
            "verification reservation is not supported by this store",
        ))
    }
    async fn delete_verification(&self, id: &str) -> AuthResult<()>;
    async fn delete_expired_verifications(&self) -> AuthResult<usize>;
}

/// Upstream's deterministic database key for first-writer verification claims.
#[must_use]
pub fn verification_reservation_key(identifier: &str) -> (String, [u8; 32]) {
    use base64::Engine;
    use sha2::{Digest, Sha256};

    let mut hash = Sha256::new();
    hash.update(b"reserve:");
    hash.update(identifier.as_bytes());
    let digest: [u8; 32] = hash.finalize().into();
    (
        base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest),
        digest,
    )
}

/// The reservation key under the configured ID strategy, as upstream's
/// `encodeDeterministicId` (1.7.12) encodes it. Under `generateId: "uuid"`
/// the first 16 digest bytes become a version 5, RFC 9562 variant UUID so
/// the forced ID survives; other strategies keep the base64url digest.
#[must_use]
pub fn verification_reservation_id(
    identifier: &str,
    database: &crate::config::AdvancedDatabaseConfig,
) -> (String, [u8; 32]) {
    let (encoded, digest) = verification_reservation_key(identifier);
    if !matches!(
        database.generate_id,
        Some(crate::config::DatabaseIdStrategy::Uuid)
    ) {
        return (encoded, digest);
    }
    let mut bytes = [0_u8; 16];
    bytes.copy_from_slice(&digest[..16]);
    bytes[6] = (bytes[6] & 0x0f) | 0x50;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    (uuid::Uuid::from_bytes(bytes).to_string(), digest)
}

// LCOV_EXCL_START
#[cfg(test)]
mod tests {
    use super::*;

    // Upstream reference: packages/core/src/db/adapter/get-id-field.test.ts :: encodeDeterministicId (1.7.12).
    #[test]
    fn uuid_strategy_reserves_a_version_5_uuid_from_the_digest() {
        let mut database = crate::config::AdvancedDatabaseConfig::default();
        let (plain, digest) = verification_reservation_id("siwe-email-claim-a@b.c", &database);
        assert_eq!(
            (plain.clone(), digest),
            verification_reservation_key("siwe-email-claim-a@b.c")
        );
        database.generate_id = Some(crate::config::DatabaseIdStrategy::Uuid);
        let (id, same) = verification_reservation_id("siwe-email-claim-a@b.c", &database);
        assert_eq!(same, digest);
        let parsed = uuid::Uuid::parse_str(&id).unwrap();
        assert_eq!(parsed.get_version_num(), 5);
        assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
        assert_eq!(parsed.as_bytes()[..6], digest[..6]);
        assert_ne!(id, plain);
    }
}
// LCOV_EXCL_STOP