Skip to content

Add OAuth grants support (list + revoke)#128

Merged
felipefreitag merged 2 commits into
mainfrom
feat/oauth-grants
Jul 9, 2026
Merged

Add OAuth grants support (list + revoke)#128
felipefreitag merged 2 commits into
mainfrom
feat/oauth-grants

Conversation

@felipefreitag

@felipefreitag felipefreitag commented Jul 9, 2026

Copy link
Copy Markdown
Member

Implements the two new OAuth endpoints announced for the API:

  • GET /oauth/grantsIResend.OAuthGrantListAsync
  • DELETE /oauth/grants/:oauthGrantIdIResend.OAuthGrantRevokeAsync

Field shapes were verified against resend-openapi#70, resend-node#998, and the docs (list-grants / revoke-grant).

What's included

  • Models (src/Resend/OAuthGrant.cs): OAuthGrant (id, client_id, scopes, resource, created_at, revoked_at, revoked_reason, nested client), OAuthGrantClient (name, logo_uri), and OAuthGrantRevoked for the revoke response.
  • Interface + client: new #region OAuth Grants in IResend; implementation in a new ResendClient.OAuthGrants.cs partial, reusing the existing ListOf<T> wrapper and Execute helpers.
  • CLI: resend oauth-grant list (with -j|--json) and resend oauth-grant revoke <id>.
  • Tests: list + revoke against the fake ApiServer (new OAuthGrantController). Full suite passes on .NET 8.

Design notes / decisions for review

  • Revoke returns a body. Unlike the SDK's other *DeleteAsync methods (which return ResendResponse), this endpoint returns { object, id, revoked_at, revoked_reason }, so OAuthGrantRevokeAsync returns ResendResponse<OAuthGrantRevoked> (matching resend-node). Named Revoke rather than Delete to match the documented operation. Happy to switch to a void ...DeleteAsync if you'd rather match the delete convention.
  • Dates as DateTime. The docs show created_at in a Postgres-style format (2026-04-08 00:11:13.110779+00) while revoke uses ISO; I verified both parse correctly through the existing JsonUtcDateTimeConverter, so I kept the idiomatic DateTime/DateTime? typing rather than opaque strings.
  • Pagination not surfaced. GET /oauth/grants supports limit/after/before and returns has_more, but the SDK's list convention (ListOf<T>) drops the wrapper and existing list methods (api-keys, domains) take no paging params. Kept consistent; surfacing pagination would be a broader SDK change across all list endpoints.
  • oauthGrantId typed Guid to match the SDK's id convention (ids are UUIDs in practice), though the OpenAPI path param is an unconstrained string.

Note: resend-openapi#70 and resend-node#998 are still open, so shapes could shift before GA.


Summary by cubic

Add OAuth grants support to the SDK and CLI: list grants with pagination and revoke a grant. This lets users see connected clients and invalidate their tokens when needed.

  • New Features

    • API: OAuthGrantListAsync(PaginatedQuery?)ResendResponse<PaginatedResult<OAuthGrant>>; OAuthGrantRevokeAsync(Guid)ResendResponse<OAuthGrantRevoked>.
    • Models: OAuthGrant, OAuthGrantClient, OAuthGrantRevoked (includes object discriminator).
    • Client: implementation in ResendClient.OAuthGrants.cs; interface updates in IResend.
    • CLI: resend oauth-grant list (supports --json) and resend oauth-grant revoke <id>.
    • Tests: list + revoke tests and a fake ApiServer controller.
  • Notes

    • List now supports pagination (limit, before, after) and returns has_more.
    • Revoke returns a body; method returns ResendResponse<OAuthGrantRevoked> and is named “Revoke”.
    • IDs are Guid; date fields use the existing UTC DateTime converter.
    • CLI output escapes Spectre markup for API-provided fields.

Written for commit 0396a9e. Summary will update on new commits.

Review in cubic

Implements the two new OAuth endpoints:
- GET /oauth/grants           -> OAuthGrantListAsync
- DELETE /oauth/grants/:id     -> OAuthGrantRevokeAsync

Adds the OAuthGrant / OAuthGrantClient / OAuthGrantRevoked models, the
IResend methods, the ResendClient implementation, CLI commands
(oauth-grant list|revoke), a fake ApiServer controller, and tests.

Field shapes follow resend-openapi#70 and resend-node#998.
@cubic-dev-ai

cubic-dev-ai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Running ultrareview automatically — This PR adds OAuth grant list and revoke endpoints, which involve authentication/authorization logic and public API contract changes, where a subtle bug could lead to security issues or data integrity problems.. I'll post findings when complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ultrareview completed in 10m 23s

2 issues found across 9 files

Confidence score: 3/5

  • In src/Resend/OAuthGrant.cs, modeling grant identifiers (id, client_id, revoke response id, and path id) as Guid while the API contract says string can cause deserialization or request-construction failures when non-GUID values appear, leading to broken OAuth grant flows for valid API responses — align these fields and route parameters to string before merging.
  • In src/Resend/OAuthGrant.cs, exposing a public Resource member that is not in the current schema can mislead SDK users into depending on data the API does not return, creating avoidable integration confusion and future breaking cleanup — remove or clearly mark it as unsupported before release.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/Resend/OAuthGrant.cs">

<violation number="1" location="src/Resend/OAuthGrant.cs:20">
P2: The OAuth grant identifiers are narrower than the current API contract: the OpenAPI schema defines the grant id, `client_id`, revoke response id, and path parameter as plain strings, but the model uses `Guid`. If the API returns or accepts a non-UUID string identifier, list/revoke calls will fail in the SDK before the caller can handle the response. Consider using `string` for the OAuth grant id fields and matching revoke parameter unless the API contract is tightened to `format: uuid`.</violation>

<violation number="2" location="src/Resend/OAuthGrant.cs:33">
P3: This adds a public `Resource` member that is no longer in the current OAuth grant schema. Keeping it in the first release of the SDK shape can mislead callers into depending on a field the API does not return, and it becomes a public API member that is harder to remove later. Consider dropping `Resource` unless the endpoint contract is restored to include it.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tools/Resend.Cli/OAuthGrant/OAuthGrantListCommand.cs Outdated
Comment thread src/Resend/OAuthGrant.cs
/// Identifier of the OAuth client the grant was issued to.
/// </summary>
[JsonPropertyName( "client_id" )]
public Guid ClientId { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The OAuth grant identifiers are narrower than the current API contract: the OpenAPI schema defines the grant id, client_id, revoke response id, and path parameter as plain strings, but the model uses Guid. If the API returns or accepts a non-UUID string identifier, list/revoke calls will fail in the SDK before the caller can handle the response. Consider using string for the OAuth grant id fields and matching revoke parameter unless the API contract is tightened to format: uuid.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Resend/OAuthGrant.cs, line 20:

<comment>The OAuth grant identifiers are narrower than the current API contract: the OpenAPI schema defines the grant id, `client_id`, revoke response id, and path parameter as plain strings, but the model uses `Guid`. If the API returns or accepts a non-UUID string identifier, list/revoke calls will fail in the SDK before the caller can handle the response. Consider using `string` for the OAuth grant id fields and matching revoke parameter unless the API contract is tightened to `format: uuid`.</comment>

<file context>
@@ -0,0 +1,108 @@
+    /// Identifier of the OAuth client the grant was issued to.
+    /// </summary>
+    [JsonPropertyName( "client_id" )]
+    public Guid ClientId { get; set; }
+
+    /// <summary>
</file context>

Comment thread src/Resend/ResendClient.OAuthGrants.cs Outdated
Comment thread src/Resend/OAuthGrant.cs
Comment thread src/Resend/OAuthGrant.cs
/// </summary>
[JsonPropertyName( "resource" )]
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
public string? Resource { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This adds a public Resource member that is no longer in the current OAuth grant schema. Keeping it in the first release of the SDK shape can mislead callers into depending on a field the API does not return, and it becomes a public API member that is harder to remove later. Consider dropping Resource unless the endpoint contract is restored to include it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Resend/OAuthGrant.cs, line 33:

<comment>This adds a public `Resource` member that is no longer in the current OAuth grant schema. Keeping it in the first release of the SDK shape can mislead callers into depending on a field the API does not return, and it becomes a public API member that is harder to remove later. Consider dropping `Resource` unless the endpoint contract is restored to include it.</comment>

<file context>
@@ -0,0 +1,108 @@
+    /// </summary>
+    [JsonPropertyName( "resource" )]
+    [JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
+    public string? Resource { get; set; }
+
+    /// <summary>
</file context>

Comment thread tools/Resend.ApiServer/Controllers/OAuthGrantController.cs Outdated
…rkup

- OAuthGrantListAsync now takes PaginatedQuery and returns
  PaginatedResult<OAuthGrant>, matching the SDK's paginated list pattern
  (limit/before/after + has_more) instead of a bare List.
- OAuthGrantRevoked exposes the 'object' discriminator, like other
  result models (AutomationStopResult, etc.).
- Fake ApiServer list endpoint returns PaginatedResult with has_more.
- Escape API-provided name/scopes before Spectre markup rendering.

Kept Guid ids (SDK-wide convention; grant/client ids are UUIDs) and the
'resource' field (present in resend-node#998 / resend-openapi#70).
@felipefreitag

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed in 0396a9e:

Fixed (4):

  • PaginationOAuthGrantListAsync now takes PaginatedQuery? and returns PaginatedResult<OAuthGrant> (limit/before/after + has_more), matching the SDK's paginated-list pattern (Topics/Contacts/etc.). Good catch — this is the right convention for a paginated endpoint.
  • Revoke object — added the Object discriminator to OAuthGrantRevoked, consistent with AutomationStopResult and other result models.
  • Fake server has_more — the ApiServer list endpoint now returns PaginatedResult<OAuthGrant>.
  • CLI markupname/scopes are now Markup.Escaped before rendering.

Kept as-is (2), with reasoning:

  • Guid vs string ids — every id across this SDK is Guid (ApiKey, Domain, Topic, Contact, …); those OpenAPI ids are also untyped strings but UUIDs in practice, and grant/client ids are UUIDs in all examples. Changing only OAuth to string would be inconsistent. Happy to revisit SDK-wide if Resend ever issues non-UUID ids.
  • resource field — it is in the current contract: resend-node#998 defines resource: string | null on OAuthGrant, and it's in resend-openapi#70. Kept to match the reference SDK.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 6 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Re-trigger cubic

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.

2 participants