better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![allow(
    clippy::expect_used,
    reason = "Fixture setup and response assertions require successful fallible construction."
)]
use super::{ApiKeyEndpoint, ApiKeyPlugin, ApiKeyVerificationError, VerifyApiKey};
use crate::plugins::api_key::{ApiKeyConfig, ApiKeyValidator};
use async_trait::async_trait;
use better_auth_core::{
    AuthConfig, AuthContext, AuthError, AuthResponse, AuthResult, FieldDate, FieldMap, FieldObject,
    FieldValue,
    observability::{LogArgument, LogLevel, LogSink},
    store::{ApiKeyStore, EphemeralStore, schema::EntityRole},
};
use serde_json::{Value, json};
use std::sync::{
    Arc, Mutex,
    atomic::{AtomicUsize, Ordering},
};

#[derive(Clone, Copy, Debug)]
enum Failure {
    Ordinary,
    CyclicJson,
    Range,
    DataClone,
    Api(u16),
    StructuralApi,
    BetterCallApi,
}

impl Failure {
    fn error(self) -> AuthResult<AuthError> {
        Ok(match self {
            Self::Ordinary => AuthError::internal("Validator operation failed"),
            Self::CyclicJson => {
                let object = FieldObject::from(FieldMap::new());
                let _ = object.set("self", FieldValue::Object(object.clone()))?;
                let result = FieldValue::Object(object.clone()).stringify();
                let _ = object.delete("self")?;
                match result {
                    Err(error @ AuthError::TypeError(_)) => error,
                    other => {
                        return Err(AuthError::internal(format!(
                            "Cyclic JSON must preserve a TypeError, got {other:?}"
                        )));
                    }
                }
            }
            Self::Range => AuthError::RangeError("Invalid array length".into()),
            Self::DataClone => AuthError::DataClone,
            Self::Api(status) => AuthResponse::json(status, &api_error_body())?.into(),
            Self::StructuralApi => AuthError::from_thrown(
                FieldMap::from([
                    ("name".into(), "APIError".into()),
                    ("statusCode".into(), 409.0.into()),
                    ("body".into(), api_error_body().into()),
                ])
                .into(),
            ),
            Self::BetterCallApi => AuthError::try_api_error(better_auth_core::ApiErrorInit {
                status: "CONFLICT".into(),
                body: api_error_body().into(),
                class: better_auth_core::ApiErrorClass::BetterCall,
                ..Default::default()
            })?,
        })
    }

    fn api_status(self) -> Option<u16> {
        match self {
            Self::Api(status) => Some(status),
            Self::StructuralApi | Self::BetterCallApi => Some(409),
            _ => None,
        }
    }
}

fn api_error_body() -> Value {
    json!({
        "code": "VALIDATOR_REJECTED",
        "message": "Validator rejected the key",
        "details": {"retry": false, "reason": ["policy", "owner"]}
    })
}

fn error_pointer(error: &AuthError) -> usize {
    match error {
        AuthError::Internal(message)
        | AuthError::TypeError(message)
        | AuthError::RangeError(message) => message.as_ptr() as usize,
        _ => 0,
    }
}

#[derive(Default)]
struct Trace {
    events: Mutex<Vec<Value>>,
    original: AtomicUsize,
}

impl Trace {
    fn push(&self, event: Value) -> AuthResult<()> {
        self.events
            .lock()
            .map_err(|_| AuthError::internal("Validator trace lock poisoned"))?
            .push(event);
        Ok(())
    }
}

impl LogSink for Trace {
    #[expect(
        clippy::expect_used,
        reason = "The contract requires the original AuthError and an available trace capture."
    )]
    fn log(
        &self,
        level: LogLevel,
        message: LogArgument<'_>,
        arguments: &[LogArgument<'_>],
    ) -> better_auth_core::AuthResult<()> {
        assert_eq!(level, LogLevel::Error);
        assert_eq!(arguments.len(), 1);
        let error = match arguments.first() {
            Some(LogArgument::Error(error)) => error.downcast_ref::<AuthError>(),
            _ => None,
        }
        .expect("Validator error must retain its original type");
        assert_eq!(error_pointer(error), self.original.load(Ordering::SeqCst));
        self.push(json!(["log", "error", message.to_string(), true]))
            .expect("Validator trace lock is not poisoned");
        Ok(())
    }
}

struct RejectingValidator {
    failure: Failure,
    trace: Arc<Trace>,
}

#[async_trait]
impl ApiKeyValidator for RejectingValidator {
    async fn validate(&self, key: &str, ctx: ApiKeyEndpoint<'_>) -> AuthResult<bool> {
        self.trace
            .push(json!(["validator:enter", key, ctx.body.json()?]))?;
        let updated = ctx
            .api_keys
            .update_api_key_record(
                &"key-id".to_owned().into(),
                [
                    ("name".into(), "After".into()),
                    ("updatedAt".into(), date().into()),
                ]
                .into(),
            )
            .await?;
        assert_eq!(updated, Some(key_fields("After")));
        self.trace.push(json!(["validator:persisted"]))?;
        let error = self.failure.error()?;
        self.trace
            .original
            .store(error_pointer(&error), Ordering::SeqCst);
        Err(error)
    }
}

fn date() -> FieldDate {
    FieldDate::from_milliseconds(1_893_456_000_000.0)
}

fn key_fields(name: &str) -> FieldMap {
    [
        ("id".into(), "key-id".into()),
        ("name".into(), name.into()),
        ("start".into(), FieldValue::Null),
        ("prefix".into(), FieldValue::Null),
        ("key".into(), "validator-secret".into()),
        ("referenceId".into(), "owner".into()),
        ("configId".into(), "default".into()),
        ("refillInterval".into(), FieldValue::Null),
        ("refillAmount".into(), FieldValue::Null),
        ("lastRefillAt".into(), FieldValue::Null),
        ("enabled".into(), true.into()),
        ("rateLimitEnabled".into(), true.into()),
        ("rateLimitTimeWindow".into(), 60_000.into()),
        ("rateLimitMax".into(), 3.into()),
        ("requestCount".into(), 0.into()),
        ("remaining".into(), 5.into()),
        ("lastRequest".into(), FieldValue::Null),
        ("expiresAt".into(), FieldValue::Null),
        ("createdAt".into(), date().into()),
        ("updatedAt".into(), date().into()),
        ("permissions".into(), FieldValue::Null),
        ("metadata".into(), FieldValue::Null),
    ]
    .into()
}

fn stored_key_fields(name: &str) -> FieldMap {
    let mut fields = key_fields(name);
    let _ = fields.insert("metadata".into(), "null".into());
    fields
}

#[tokio::test]
async fn validator_errors_preserve_catch_scope_response_and_committed_callback_write()
-> AuthResult<()> {
    for failure in [
        Failure::Ordinary,
        Failure::CyclicJson,
        Failure::Range,
        Failure::DataClone,
        Failure::Api(403),
        Failure::Api(500),
        Failure::StructuralApi,
        Failure::BetterCallApi,
    ] {
        for scoped in [false, true] {
            let trace = Arc::new(Trace::default());
            let mut config = AuthConfig::default();
            {
                let better_auth_core::LoggerSource::Config(logger_options) = &mut config.logger
                else {
                    return Err(AuthError::config(
                        "Fixture requires authored logger options",
                    ));
                };
                logger_options.log = Some(trace.clone());
            }
            let config = Arc::new(config);
            let store = Arc::new(EphemeralStore::new(config.clone()));
            assert_eq!(
                store.create_api_key_record(key_fields("Before")).await?,
                Some(key_fields("Before"))
            );
            assert_eq!(
                store.plugin_storage_rows(EntityRole::ApiKey)?,
                [stored_key_fields("Before")]
            );
            let ctx = AuthContext::new(config, store.clone()).expect("fixture context initializes");
            let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
                disable_key_hashing: true,
                custom_api_key_validator: Some(Arc::new(RejectingValidator {
                    failure,
                    trace: trace.clone(),
                })),
                ..Default::default()
            });
            let error = match plugin
                .verify_api_key(
                    &VerifyApiKey {
                        key: "validator-secret",
                        config_id: scoped.then_some("default"),
                        permissions: None,
                    },
                    &ctx,
                )
                .await
            {
                Err(error) => error,
                Ok(key) => {
                    return Err(AuthError::internal(format!(
                        "Validator must reject {failure:?}, got {key:?}"
                    )));
                }
            };
            trace.push(json!([if scoped {
                "consumer:threw"
            } else {
                "consumer:returned"
            }]))?;
            if scoped {
                let ApiKeyVerificationError::Endpoint(error) = error else {
                    return Err(AuthError::internal(format!(
                        "Scoped validator must escape the catch boundary: {error:?}"
                    )));
                };
                assert_eq!(error_pointer(&error), trace.original.load(Ordering::SeqCst));
                if let Some(status) = failure.api_status() {
                    let response = error.to_auth_response().expect("fixture response succeeds");
                    assert_eq!(
                        response.api_error_status()?,
                        better_auth_core::SchemaValue::Typed(status)
                    );
                    assert_eq!(response.api_error_body()?.json()?, Some(api_error_body()));
                } else {
                    assert!(!error.is_api_error().unwrap());
                }
            } else {
                let is_api = failure.api_status().is_some();
                assert_eq!(
                    matches!(error, ApiKeyVerificationError::Rejected(_)),
                    is_api
                );
                let response = error.into_response()?;
                assert_eq!(response.status, 200);
                assert_eq!(
                    response.body.json()?,
                    Some(json!({
                        "valid": false,
                        "error": if is_api { api_error_body() } else { json!({
                            "code": "INVALID_API_KEY",
                            "message": {"code": "INVALID_API_KEY", "message": "Invalid API key."}
                        }) },
                        "key": null
                    }))
                );
            }
            assert_eq!(
                store.plugin_storage_rows(EntityRole::ApiKey)?,
                [stored_key_fields("After")]
            );
            let body = if scoped {
                json!({"key": "validator-secret", "configId": "default"})
            } else {
                json!({"key": "validator-secret"})
            };
            let mut expected = vec![
                json!(["validator:enter", "validator-secret", body]),
                json!(["validator:persisted"]),
            ];
            if !scoped {
                expected.push(json!(["log", "error", "Failed to validate API key:", true]));
            }
            expected.push(json!([if scoped {
                "consumer:threw"
            } else {
                "consumer:returned"
            }]));
            assert_eq!(
                *trace
                    .events
                    .lock()
                    .map_err(|_| AuthError::internal("Validator trace lock poisoned"))?,
                expected,
                "{failure:?}, scoped={scoped}"
            );
        }
    }
    Ok(())
}