Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## Unreleased

### Added

- `list` and `revoke` oauth endpoints

### Changed

- `Log.id` is now a new `LogId` type instead of a `String`
Expand Down
6 changes: 5 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
config::Config,
events::EventsSvc,
logs::LogsSvc,
oauth::OAuthSvc,
services::{AutomationsSvc, ReceivingSvc},
webhooks::WebhookSvc,
};
Expand Down Expand Up @@ -56,6 +57,8 @@ pub struct Resend {
pub automations: AutomationsSvc,
/// `Resend` APIs for `/events` endpoints.
pub events: EventsSvc,
/// `Resend` APIs for `/oauth` endpoints.
pub oauth: OAuthSvc,
}

impl Resend {
Expand Down Expand Up @@ -110,7 +113,8 @@ impl Resend {
webhooks: WebhookSvc(Arc::clone(&inner)),
logs: LogsSvc(Arc::clone(&inner)),
automations: AutomationsSvc(Arc::clone(&inner)),
events: EventsSvc(inner),
events: EventsSvc(Arc::clone(&inner)),
oauth: OAuthSvc(inner),
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub mod events;
pub mod idempotent;
pub mod list_opts;
mod logs;
mod oauth;
pub mod rate_limit;
mod receiving;
mod segments;
Expand All @@ -86,6 +87,7 @@ pub mod services {
pub use super::domains::DomainsSvc;
pub use super::emails::EmailsSvc;
pub use super::logs::LogsSvc;
pub use super::oauth::OAuthSvc;
pub use super::receiving::ReceivingSvc;
pub use super::segments::SegmentsSvc;
pub use super::templates::TemplateSvc;
Expand Down Expand Up @@ -146,6 +148,9 @@ pub mod types {
UpdateEventResponse,
};
pub use super::logs::types::{Log, LogId};
pub use super::oauth::types::{
ClientId, OAuthGrant, OAuthGrantClient, OAuthGrantId, RevokeOAuthGrantResponse,
};
pub use super::receiving::types::{
ForwardInboundEmailResponse, ForwardReceivingEmail, GetInboundEmailOptions,
GetInboundEmailRaw, InboundAttachment, InboundAttachmentId, InboundEmail,
Expand Down
172 changes: 172 additions & 0 deletions src/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::sync::Arc;

use reqwest::Method;

use crate::{
Config, Result,
list_opts::{ListOptions, ListResponse},
types::{OAuthGrant, RevokeOAuthGrantResponse},
};

/// `Resend` APIs for `/oauth` endpoints.
#[derive(Clone, Debug)]
pub struct OAuthSvc(pub(crate) Arc<Config>);

impl OAuthSvc {
/// Retrieve a list of OAuth grants for the authenticated team.
///
/// - Default limit: *infinite*
///
/// <https://resend.com/docs/api-reference/oauth/list-grants>
#[maybe_async::maybe_async]
#[allow(clippy::needless_pass_by_value)]
pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<OAuthGrant>> {
let request = self.0.build(Method::GET, "/oauth/grants").query(&list_opts);
let response = self.0.send(request).await?;
let content = response.json::<ListResponse<OAuthGrant>>().await?;

Ok(content)
}

/// Revoke an OAuth grant for the authenticated team.
///
/// <https://resend.com/docs/api-reference/oauth/revoke-grant>
#[maybe_async::maybe_async]
pub async fn revoke(&self, oauth_grant_id: &str) -> Result<RevokeOAuthGrantResponse> {
let path = format!("/oauth/grants/{oauth_grant_id}");

let request = self.0.build(Method::DELETE, &path);
let response = self.0.send(request).await?;
let content = response.json::<RevokeOAuthGrantResponse>().await?;

Ok(content)
}
}

#[allow(unreachable_pub)]
pub mod types {
use serde::{Deserialize, Serialize};

crate::define_id_type!(OAuthGrantId);
crate::define_id_type!(ClientId);

#[must_use]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthGrant {
pub id: OAuthGrantId,
pub client_id: ClientId,
pub scopes: Vec<String>,
pub created_at: String,
pub revoked_at: Option<String>,
pub revoked_reason: Option<String>,
pub client: OAuthGrantClient,
}

#[must_use]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthGrantClient {
pub name: String,
pub logo_uri: Option<String>,
}

#[must_use]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevokeOAuthGrantResponse {
pub id: OAuthGrantId,
pub revoked_at: String,
pub revoked_reason: String,
}
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use crate::{
list_opts::{ListOptions, ListResponse},
types::OAuthGrant,
};
use crate::{
test::{CLIENT, DebugResult},
types::RevokeOAuthGrantResponse,
};

#[tokio_shared_rt::test(shared = true)]
#[cfg(not(feature = "blocking"))]
async fn all() -> DebugResult<()> {
let resend = &*CLIENT;

let logs = resend.oauth.list(ListOptions::default()).await?;
assert!(logs.data.is_empty());
Comment thread
AntoniosBarotsis marked this conversation as resolved.

Ok(())
}

#[test]
fn deserialize_grant() {
let grant = r#"{
"id": "650e8400-e29b-41d4-a716-446655440002",
"client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
"scopes": ["emails:send", "domains:read"],
"created_at": "2026-04-07 00:11:13.110779+00",
"revoked_at": "2026-04-09 00:11:13.110779+00",
"revoked_reason": "revoked_from_api",
"client": {
"name": "Resend CLI",
"logo_uri": "https://example.com/logo.png"
}
}"#;

let res = serde_json::from_str::<OAuthGrant>(grant);
assert!(res.is_ok());
}

#[test]
fn deserialize_list() {
let grants = r#"{
"object": "list",
"has_more": false,
"data": [
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
"scopes": ["emails:send"],
"created_at": "2026-04-08 00:11:13.110779+00",
"revoked_at": null,
"revoked_reason": null,
"client": {
"name": "Resend CLI",
"logo_uri": "https://example.com/logo.png"
}
},
{
"id": "650e8400-e29b-41d4-a716-446655440002",
"client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
"scopes": ["emails:send", "domains:read"],
"created_at": "2026-04-07 00:11:13.110779+00",
"revoked_at": "2026-04-09 00:11:13.110779+00",
"revoked_reason": "revoked_from_api",
"client": {
"name": "Resend CLI",
"logo_uri": "https://example.com/logo.png"
}
}
]
}"#;

let res = serde_json::from_str::<ListResponse<OAuthGrant>>(grants);
assert!(res.is_ok(), "{:?}", res.err());
}

#[test]
fn deserialize_revoke() {
let revoke = r#"{
"object": "oauth_grant",
"id": "650e8400-e29b-41d4-a716-446655440001",
"revoked_at": "2026-04-08T00:11:13.110Z",
"revoked_reason": "revoked_from_api"
}"#;

let res = serde_json::from_str::<RevokeOAuthGrantResponse>(revoke);
assert!(res.is_ok());
}
}