From 202a61ccd6b9e9dd75600c6616a04fc7afb691cc Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Thu, 12 Nov 2026 09:03:45 -0300 Subject: [PATCH 1/2] feat(domains): add forge activation methods Co-Authored-By: Claude Fable 5 --- Gemfile.lock | 2 +- lib/resend.rb | 1 + lib/resend/domains/forge.rb | 57 ++++++++++++++++++ lib/resend/version.rb | 2 +- spec/domain_forge_spec.rb | 117 ++++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 lib/resend/domains/forge.rb create mode 100644 spec/domain_forge_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 1582b43..2641498 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - resend (1.19.0) + resend (1.20.0) base64 httparty (>= 0.22.0) diff --git a/lib/resend.rb b/lib/resend.rb index 952739d..6b7805f 100644 --- a/lib/resend.rb +++ b/lib/resend.rb @@ -27,6 +27,7 @@ require "resend/contact_properties" require "resend/domains" require "resend/domains/claims" +require "resend/domains/forge" require "resend/emails" require "resend/templates" require "resend/emails/receiving" diff --git a/lib/resend/domains/forge.rb b/lib/resend/domains/forge.rb new file mode 100644 index 0000000..ae07754 --- /dev/null +++ b/lib/resend/domains/forge.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Resend + module Domains + # Domain Forge API wrapper + module Forge + class << self + # + # Activate Forge on a domain. Returns the full domain, in the same shape + # as Resend::Domains.get. Enabling a domain that is already migrated is + # idempotent and returns its current state. + # + # @param params [Hash] the parameters + # @option params [String] :id the ID of the domain (required) + # @option params [String] :return_path subdomain for the Return-Path address. + # When omitted, the API picks a suggested label based on the domain's + # current return path. + # + # https://resend.com/docs/api-reference/domains/enable-forge + def enable(params) + path = "domains/#{params[:id]}/forge" + Resend::Request.new(path, params.slice(:return_path), "post").perform + end + + # + # Move the Return-Path of a Forge domain and re-verify it. Returns the + # full domain. After the domain verifies, the API refuses this with a + # 422 'return_path_unavailable'. + # + # @param params [Hash] the parameters + # @option params [String] :id the ID of the domain (required) + # @option params [String] :return_path subdomain for the Return-Path address (required) + # + # https://resend.com/docs/api-reference/domains/update-forge + def update(params) + raise ArgumentError, "return_path is required" if params[:return_path].nil? + + path = "domains/#{params[:id]}/forge" + Resend::Request.new(path, params.slice(:return_path), "patch").perform + end + + # + # Trigger asynchronous Forge verification for a domain. Answers with the + # `{ object: 'domain', id }` pair like Resend::Domains.verify; poll + # Resend::Domains.get for state. + # + # @param domain_id [String] the ID of the domain + # + # https://resend.com/docs/api-reference/domains/verify-forge + def verify(domain_id = "") + path = "domains/#{domain_id}/forge/verify" + Resend::Request.new(path, {}, "post").perform + end + end + end + end +end diff --git a/lib/resend/version.rb b/lib/resend/version.rb index e3e06b5..7dc8c61 100644 --- a/lib/resend/version.rb +++ b/lib/resend/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Resend - VERSION = "1.19.0" + VERSION = "1.20.0" end diff --git a/spec/domain_forge_spec.rb b/spec/domain_forge_spec.rb new file mode 100644 index 0000000..d9f8573 --- /dev/null +++ b/spec/domain_forge_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +RSpec.describe "Domains::Forge" do + before do + Resend.configure do |config| + config.api_key = "re_123" + end + end + + let(:domain_id) { "d91cd9bd-1176-453e-8fc1-35364d380206" } + + let(:forge_domain) do + { + object: "domain", + id: "d91cd9bd-1176-453e-8fc1-35364d380206", + name: "example.com", + status: "pending", + created_at: "2026-11-11 10:00:00.000000+00", + region: "us-east-1", + capabilities: { + "sending" => "enabled", + "receiving" => "disabled" + }, + records: [ + { + "record" => "SPF", + "name" => "send", + "value" => "v=spf1 include:spf.resend.com ~all", + "type" => "TXT", + "ttl" => "Auto", + "status" => "pending" + }, + { + "record" => "DKIM", + "name" => "resend._domainkey", + "value" => "resend._domainkey.example.com.dkim.resend.com.", + "type" => "CNAME", + "ttl" => "Auto", + "status" => "pending" + } + ], + open_tracking: false, + click_tracking: false + } + end + + describe "enable" do + it "should activate Forge and return the full domain" do + params = { id: domain_id, return_path: "send" } + + expect(Resend::Request).to receive(:new) + .with("domains/#{domain_id}/forge", { return_path: "send" }, "post") + .and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(forge_domain) + + domain = Resend::Domains::Forge.enable(params) + + expect(domain[:object]).to eql("domain") + expect(domain[:id]).to eql(domain_id) + expect(domain[:status]).to eql("pending") + expect(domain[:records].first["record"]).to eql("SPF") + end + + it "should activate Forge without a return path" do + expect(Resend::Request).to receive(:new) + .with("domains/#{domain_id}/forge", {}, "post") + .and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(forge_domain) + + domain = Resend::Domains::Forge.enable({ id: domain_id }) + + expect(domain[:object]).to eql("domain") + expect(domain[:id]).to eql(domain_id) + end + end + + describe "update" do + it "should move the return path" do + params = { id: domain_id, return_path: "outbound" } + + expect(Resend::Request).to receive(:new) + .with("domains/#{domain_id}/forge", { return_path: "outbound" }, "patch") + .and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(forge_domain) + + domain = Resend::Domains::Forge.update(params) + + expect(domain[:object]).to eql("domain") + expect(domain[:status]).to eql("pending") + end + + it "should raise when return_path is missing" do + expect do + Resend::Domains::Forge.update({ id: domain_id }) + end.to raise_error(ArgumentError, "return_path is required") + end + end + + describe "verify" do + it "should trigger an asynchronous Forge verification" do + resp = { + object: "domain", + id: domain_id + } + + expect(Resend::Request).to receive(:new) + .with("domains/#{domain_id}/forge/verify", {}, "post") + .and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + verified = Resend::Domains::Forge.verify(domain_id) + + expect(verified[:object]).to eql("domain") + expect(verified[:id]).to eql(domain_id) + end + end +end From eec4efec7f78eaa428a944fc6f5a29cad8b522cb Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Thu, 12 Nov 2026 13:26:45 -0300 Subject: [PATCH 2/2] feat(domains): fold forge verify and update into the classic endpoints Matches the reshaped API: Domains::Forge keeps only enable (return_path optional), Domains.verify gains an optional scope: ["forge"] that re-checks only the Forge record, and Domains.update accepts forge_return_path to move the Forge return path without starting verification. Co-Authored-By: Claude Fable 5 --- lib/resend/domains.rb | 10 +++++++-- lib/resend/domains/forge.rb | 33 +-------------------------- spec/domain_forge_spec.rb | 45 ++----------------------------------- spec/domains_spec.rb | 37 ++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 77 deletions(-) diff --git a/lib/resend/domains.rb b/lib/resend/domains.rb index f90516c..7b4dea1 100644 --- a/lib/resend/domains.rb +++ b/lib/resend/domains.rb @@ -10,6 +10,9 @@ def create(params) Resend::Request.new(path, params, "post").perform end + # Updates a domain. :forge_return_path moves the Forge return path + # without starting verification, and composes with the other fields. + # # https://resend.com/docs/api-reference/domains/update-domain def update(params) path = "domains/#{params[:id]}" @@ -34,10 +37,13 @@ def remove(domain_id = "") Resend::Request.new(path, {}, "delete").perform end + # Triggers domain verification. Pass `scope: ["forge"]` to re-check only + # the Forge record; without a scope the whole domain is verified. + # # https://resend.com/docs/api-reference/domains/verify-domain - def verify(domain_id = "") + def verify(domain_id = "", params = {}) path = "domains/#{domain_id}/verify" - Resend::Request.new(path, {}, "post").perform + Resend::Request.new(path, params.slice(:scope), "post").perform end end end diff --git a/lib/resend/domains/forge.rb b/lib/resend/domains/forge.rb index ae07754..fb550bf 100644 --- a/lib/resend/domains/forge.rb +++ b/lib/resend/domains/forge.rb @@ -13,44 +13,13 @@ class << self # @param params [Hash] the parameters # @option params [String] :id the ID of the domain (required) # @option params [String] :return_path subdomain for the Return-Path address. - # When omitted, the API picks a suggested label based on the domain's - # current return path. + # When omitted, the server picks a free DNS label. # # https://resend.com/docs/api-reference/domains/enable-forge def enable(params) path = "domains/#{params[:id]}/forge" Resend::Request.new(path, params.slice(:return_path), "post").perform end - - # - # Move the Return-Path of a Forge domain and re-verify it. Returns the - # full domain. After the domain verifies, the API refuses this with a - # 422 'return_path_unavailable'. - # - # @param params [Hash] the parameters - # @option params [String] :id the ID of the domain (required) - # @option params [String] :return_path subdomain for the Return-Path address (required) - # - # https://resend.com/docs/api-reference/domains/update-forge - def update(params) - raise ArgumentError, "return_path is required" if params[:return_path].nil? - - path = "domains/#{params[:id]}/forge" - Resend::Request.new(path, params.slice(:return_path), "patch").perform - end - - # - # Trigger asynchronous Forge verification for a domain. Answers with the - # `{ object: 'domain', id }` pair like Resend::Domains.verify; poll - # Resend::Domains.get for state. - # - # @param domain_id [String] the ID of the domain - # - # https://resend.com/docs/api-reference/domains/verify-forge - def verify(domain_id = "") - path = "domains/#{domain_id}/forge/verify" - Resend::Request.new(path, {}, "post").perform - end end end end diff --git a/spec/domain_forge_spec.rb b/spec/domain_forge_spec.rb index d9f8573..97cee58 100644 --- a/spec/domain_forge_spec.rb +++ b/spec/domain_forge_spec.rb @@ -45,7 +45,7 @@ end describe "enable" do - it "should activate Forge and return the full domain" do + it "activates Forge and returns the full domain" do params = { id: domain_id, return_path: "send" } expect(Resend::Request).to receive(:new) @@ -61,7 +61,7 @@ expect(domain[:records].first["record"]).to eql("SPF") end - it "should activate Forge without a return path" do + it "activates Forge without a return path" do expect(Resend::Request).to receive(:new) .with("domains/#{domain_id}/forge", {}, "post") .and_call_original @@ -73,45 +73,4 @@ expect(domain[:id]).to eql(domain_id) end end - - describe "update" do - it "should move the return path" do - params = { id: domain_id, return_path: "outbound" } - - expect(Resend::Request).to receive(:new) - .with("domains/#{domain_id}/forge", { return_path: "outbound" }, "patch") - .and_call_original - allow_any_instance_of(Resend::Request).to receive(:perform).and_return(forge_domain) - - domain = Resend::Domains::Forge.update(params) - - expect(domain[:object]).to eql("domain") - expect(domain[:status]).to eql("pending") - end - - it "should raise when return_path is missing" do - expect do - Resend::Domains::Forge.update({ id: domain_id }) - end.to raise_error(ArgumentError, "return_path is required") - end - end - - describe "verify" do - it "should trigger an asynchronous Forge verification" do - resp = { - object: "domain", - id: domain_id - } - - expect(Resend::Request).to receive(:new) - .with("domains/#{domain_id}/forge/verify", {}, "post") - .and_call_original - allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) - - verified = Resend::Domains::Forge.verify(domain_id) - - expect(verified[:object]).to eql("domain") - expect(verified[:id]).to eql(domain_id) - end - end end diff --git a/spec/domains_spec.rb b/spec/domains_spec.rb index bbcf7b8..991a25c 100644 --- a/spec/domains_spec.rb +++ b/spec/domains_spec.rb @@ -199,6 +199,27 @@ domain = Resend::Domains.update(update_params) expect(domain[:id]).to eql("479e3145-dd38-476b-932c-529ceb705947") end + + it "updates the forge return path" do + resp = { + "object": "domain", + "id": "479e3145-dd38-476b-932c-529ceb705947" + } + + update_params = { + id: "479e3145-dd38-476b-932c-529ceb705947", + forge_return_path: "outbound" + } + + expect(Resend::Request).to receive(:new) + .with("domains/479e3145-dd38-476b-932c-529ceb705947", update_params, "patch") + .and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + domain = Resend::Domains.update(update_params) + expect(domain[:id]).to eql("479e3145-dd38-476b-932c-529ceb705947") + expect(domain[:object]).to eql("domain") + end end describe "get domain" do @@ -278,5 +299,21 @@ allow_any_instance_of(Resend::Request).to receive(:perform).and_return("") expect { Resend::Domains.verify }.not_to raise_error end + + it "verifies only the Forge record when scoped" do + resp = { + "object": "domain", + "id": "d91cd9bd-1176-453e-8fc1-35364d380206" + } + + expect(Resend::Request).to receive(:new) + .with("domains/d91cd9bd-1176-453e-8fc1-35364d380206/verify", { scope: ["forge"] }, "post") + .and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + verified = Resend::Domains.verify("d91cd9bd-1176-453e-8fc1-35364d380206", scope: ["forge"]) + expect(verified[:object]).to eql("domain") + expect(verified[:id]).to eql("d91cd9bd-1176-453e-8fc1-35364d380206") + end end end