resend 0.27.0.alpha.1 → 0.27.0.alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/resend/{attachments/receiving.rb → emails/attachments.rb} +15 -17
- data/lib/resend/emails/receiving/attachments.rb +69 -0
- data/lib/resend/templates.rb +50 -0
- data/lib/resend/version.rb +1 -1
- data/lib/resend/webhooks.rb +239 -0
- data/lib/resend.rb +4 -1
- metadata +6 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 4133247b039437fb66f5b38abbe61c41d9cc85e007ae7ef7a00b865af21f9875
         | 
| 4 | 
            +
              data.tar.gz: ab112b1b77f4f7023fe98c2ab23fa93448d1b1700d7c3ea3dbf68f78ea69e214
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: b525a8dabb6d312585fa59db58e17cf8982f3ac5603502a62046ce24211b3ba59c056361e80ac7a958d6d073cea940fece2493c320b5ac7842eed88e34b329c3
         | 
| 7 | 
            +
              data.tar.gz: 38a6ce6faf1b907ee646bf53f28f8f0b67b7d1174e3a2d7fd075879c24a2399817d820ed8f0a2e19f50251f84a3ad7d2e0327a669f4e368e45f531cc3413b43a
         | 
| @@ -1,11 +1,11 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Resend
         | 
| 4 | 
            -
              module  | 
| 5 | 
            -
                # Module for  | 
| 6 | 
            -
                module  | 
| 4 | 
            +
              module Emails
         | 
| 5 | 
            +
                # Module for sent email attachments API operations
         | 
| 6 | 
            +
                module Attachments
         | 
| 7 7 | 
             
                  class << self
         | 
| 8 | 
            -
                    # Retrieve a single attachment from a  | 
| 8 | 
            +
                    # Retrieve a single attachment from a sent email
         | 
| 9 9 | 
             
                    #
         | 
| 10 10 | 
             
                    # @param params [Hash] Parameters for retrieving the attachment
         | 
| 11 11 | 
             
                    # @option params [String] :id The attachment ID (required)
         | 
| @@ -13,7 +13,7 @@ module Resend | |
| 13 13 | 
             
                    # @return [Hash] The attachment object
         | 
| 14 14 | 
             
                    #
         | 
| 15 15 | 
             
                    # @example
         | 
| 16 | 
            -
                    #   Resend::Attachments | 
| 16 | 
            +
                    #   Resend::Emails::Attachments.get(
         | 
| 17 17 | 
             
                    #     id: "2a0c9ce0-3112-4728-976e-47ddcd16a318",
         | 
| 18 18 | 
             
                    #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
         | 
| 19 19 | 
             
                    #   )
         | 
| @@ -21,11 +21,11 @@ module Resend | |
| 21 21 | 
             
                      attachment_id = params[:id]
         | 
| 22 22 | 
             
                      email_id = params[:email_id]
         | 
| 23 23 |  | 
| 24 | 
            -
                      path = "emails | 
| 24 | 
            +
                      path = "emails/#{email_id}/attachments/#{attachment_id}"
         | 
| 25 25 | 
             
                      Resend::Request.new(path, {}, "get").perform
         | 
| 26 26 | 
             
                    end
         | 
| 27 27 |  | 
| 28 | 
            -
                    # List attachments from a  | 
| 28 | 
            +
                    # List attachments from a sent email with optional pagination
         | 
| 29 29 | 
             
                    #
         | 
| 30 30 | 
             
                    # @param params [Hash] Parameters for listing attachments
         | 
| 31 31 | 
             
                    # @option params [String] :email_id The email ID (required)
         | 
| @@ -35,33 +35,31 @@ module Resend | |
| 35 35 | 
             
                    # @return [Hash] List of attachments with pagination info
         | 
| 36 36 | 
             
                    #
         | 
| 37 37 | 
             
                    # @example List all attachments
         | 
| 38 | 
            -
                    #   Resend::Attachments | 
| 38 | 
            +
                    #   Resend::Emails::Attachments.list(
         | 
| 39 39 | 
             
                    #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
         | 
| 40 40 | 
             
                    #   )
         | 
| 41 41 | 
             
                    #
         | 
| 42 42 | 
             
                    # @example List with custom limit
         | 
| 43 | 
            -
                    #   Resend::Attachments | 
| 43 | 
            +
                    #   Resend::Emails::Attachments.list(
         | 
| 44 44 | 
             
                    #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
         | 
| 45 45 | 
             
                    #     limit: 50
         | 
| 46 46 | 
             
                    #   )
         | 
| 47 47 | 
             
                    #
         | 
| 48 48 | 
             
                    # @example List with pagination
         | 
| 49 | 
            -
                    #   Resend::Attachments | 
| 49 | 
            +
                    #   Resend::Emails::Attachments.list(
         | 
| 50 50 | 
             
                    #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
         | 
| 51 51 | 
             
                    #     limit: 20,
         | 
| 52 52 | 
             
                    #     after: "attachment_id_123"
         | 
| 53 53 | 
             
                    #   )
         | 
| 54 54 | 
             
                    def list(params = {})
         | 
| 55 55 | 
             
                      email_id = params[:email_id]
         | 
| 56 | 
            -
                       | 
| 56 | 
            +
                      base_path = "emails/#{email_id}/attachments"
         | 
| 57 57 |  | 
| 58 | 
            -
                      #  | 
| 59 | 
            -
                       | 
| 60 | 
            -
                      query_params[:limit] = params[:limit] if params[:limit]
         | 
| 61 | 
            -
                      query_params[:after] = params[:after] if params[:after]
         | 
| 62 | 
            -
                      query_params[:before] = params[:before] if params[:before]
         | 
| 58 | 
            +
                      # Extract pagination parameters
         | 
| 59 | 
            +
                      pagination_params = params.slice(:limit, :after, :before)
         | 
| 63 60 |  | 
| 64 | 
            -
                      Resend:: | 
| 61 | 
            +
                      path = Resend::PaginationHelper.build_paginated_path(base_path, pagination_params)
         | 
| 62 | 
            +
                      Resend::Request.new(path, {}, "get").perform
         | 
| 65 63 | 
             
                    end
         | 
| 66 64 | 
             
                  end
         | 
| 67 65 | 
             
                end
         | 
| @@ -0,0 +1,69 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Resend
         | 
| 4 | 
            +
              module Emails
         | 
| 5 | 
            +
                module Receiving
         | 
| 6 | 
            +
                  # Module for received email attachments API operations
         | 
| 7 | 
            +
                  module Attachments
         | 
| 8 | 
            +
                    class << self
         | 
| 9 | 
            +
                      # Retrieve a single attachment from a received email
         | 
| 10 | 
            +
                      #
         | 
| 11 | 
            +
                      # @param params [Hash] Parameters for retrieving the attachment
         | 
| 12 | 
            +
                      # @option params [String] :id The attachment ID (required)
         | 
| 13 | 
            +
                      # @option params [String] :email_id The email ID (required)
         | 
| 14 | 
            +
                      # @return [Hash] The attachment object
         | 
| 15 | 
            +
                      #
         | 
| 16 | 
            +
                      # @example
         | 
| 17 | 
            +
                      #   Resend::Emails::Receiving::Attachments.get(
         | 
| 18 | 
            +
                      #     id: "2a0c9ce0-3112-4728-976e-47ddcd16a318",
         | 
| 19 | 
            +
                      #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
         | 
| 20 | 
            +
                      #   )
         | 
| 21 | 
            +
                      def get(params = {})
         | 
| 22 | 
            +
                        attachment_id = params[:id]
         | 
| 23 | 
            +
                        email_id = params[:email_id]
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                        path = "emails/receiving/#{email_id}/attachments/#{attachment_id}"
         | 
| 26 | 
            +
                        Resend::Request.new(path, {}, "get").perform
         | 
| 27 | 
            +
                      end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                      # List attachments from a received email with optional pagination
         | 
| 30 | 
            +
                      #
         | 
| 31 | 
            +
                      # @param params [Hash] Parameters for listing attachments
         | 
| 32 | 
            +
                      # @option params [String] :email_id The email ID (required)
         | 
| 33 | 
            +
                      # @option params [Integer] :limit Maximum number of attachments to return (1-100)
         | 
| 34 | 
            +
                      # @option params [String] :after Cursor for pagination (newer attachments)
         | 
| 35 | 
            +
                      # @option params [String] :before Cursor for pagination (older attachments)
         | 
| 36 | 
            +
                      # @return [Hash] List of attachments with pagination info
         | 
| 37 | 
            +
                      #
         | 
| 38 | 
            +
                      # @example List all attachments
         | 
| 39 | 
            +
                      #   Resend::Emails::Receiving::Attachments.list(
         | 
| 40 | 
            +
                      #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
         | 
| 41 | 
            +
                      #   )
         | 
| 42 | 
            +
                      #
         | 
| 43 | 
            +
                      # @example List with custom limit
         | 
| 44 | 
            +
                      #   Resend::Emails::Receiving::Attachments.list(
         | 
| 45 | 
            +
                      #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
         | 
| 46 | 
            +
                      #     limit: 50
         | 
| 47 | 
            +
                      #   )
         | 
| 48 | 
            +
                      #
         | 
| 49 | 
            +
                      # @example List with pagination
         | 
| 50 | 
            +
                      #   Resend::Emails::Receiving::Attachments.list(
         | 
| 51 | 
            +
                      #     email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
         | 
| 52 | 
            +
                      #     limit: 20,
         | 
| 53 | 
            +
                      #     after: "attachment_id_123"
         | 
| 54 | 
            +
                      #   )
         | 
| 55 | 
            +
                      def list(params = {})
         | 
| 56 | 
            +
                        email_id = params[:email_id]
         | 
| 57 | 
            +
                        base_path = "emails/receiving/#{email_id}/attachments"
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        # Extract pagination parameters
         | 
| 60 | 
            +
                        pagination_params = params.slice(:limit, :after, :before)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                        path = Resend::PaginationHelper.build_paginated_path(base_path, pagination_params)
         | 
| 63 | 
            +
                        Resend::Request.new(path, {}, "get").perform
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
            end
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Resend
         | 
| 4 | 
            +
              # Templates api wrapper
         | 
| 5 | 
            +
              module Templates
         | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                  # https://resend.com/docs/api-reference/templates/create-template
         | 
| 8 | 
            +
                  def create(params = {})
         | 
| 9 | 
            +
                    path = "templates"
         | 
| 10 | 
            +
                    Resend::Request.new(path, params, "post").perform
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # https://resend.com/docs/api-reference/templates/get-template
         | 
| 14 | 
            +
                  def get(template_id = "")
         | 
| 15 | 
            +
                    path = "templates/#{template_id}"
         | 
| 16 | 
            +
                    Resend::Request.new(path, {}, "get").perform
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  # https://resend.com/docs/api-reference/templates/update-template
         | 
| 20 | 
            +
                  def update(template_id, params = {})
         | 
| 21 | 
            +
                    path = "templates/#{template_id}"
         | 
| 22 | 
            +
                    Resend::Request.new(path, params, "patch").perform
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  # https://resend.com/docs/api-reference/templates/publish-template
         | 
| 26 | 
            +
                  def publish(template_id = "")
         | 
| 27 | 
            +
                    path = "templates/#{template_id}/publish"
         | 
| 28 | 
            +
                    Resend::Request.new(path, {}, "post").perform
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # https://resend.com/docs/api-reference/templates/duplicate-template
         | 
| 32 | 
            +
                  def duplicate(template_id = "")
         | 
| 33 | 
            +
                    path = "templates/#{template_id}/duplicate"
         | 
| 34 | 
            +
                    Resend::Request.new(path, {}, "post").perform
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # https://resend.com/docs/api-reference/templates/list-templates
         | 
| 38 | 
            +
                  def list(params = {})
         | 
| 39 | 
            +
                    path = Resend::PaginationHelper.build_paginated_path("templates", params)
         | 
| 40 | 
            +
                    Resend::Request.new(path, {}, "get").perform
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  # https://resend.com/docs/api-reference/templates/delete-template
         | 
| 44 | 
            +
                  def remove(template_id = "")
         | 
| 45 | 
            +
                    path = "templates/#{template_id}"
         | 
| 46 | 
            +
                    Resend::Request.new(path, {}, "delete").perform
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         | 
    
        data/lib/resend/version.rb
    CHANGED
    
    
| @@ -0,0 +1,239 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "openssl"
         | 
| 4 | 
            +
            require "base64"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Resend
         | 
| 7 | 
            +
              # The Webhooks module provides methods for managing webhooks via the Resend API.
         | 
| 8 | 
            +
              # Webhooks allow you to receive real-time notifications about email events.
         | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # Default tolerance for timestamp validation (5 minutes)
         | 
| 11 | 
            +
              WEBHOOK_TOLERANCE_SECONDS = 300
         | 
| 12 | 
            +
              #
         | 
| 13 | 
            +
              # @example Create a webhook
         | 
| 14 | 
            +
              #   Resend::Webhooks.create(
         | 
| 15 | 
            +
              #     endpoint: "https://webhook.example.com/handler",
         | 
| 16 | 
            +
              #     events: ["email.sent", "email.delivered", "email.bounced"]
         | 
| 17 | 
            +
              #   )
         | 
| 18 | 
            +
              #
         | 
| 19 | 
            +
              # @example List all webhooks
         | 
| 20 | 
            +
              #   Resend::Webhooks.list
         | 
| 21 | 
            +
              #
         | 
| 22 | 
            +
              # @example Retrieve a specific webhook
         | 
| 23 | 
            +
              #   Resend::Webhooks.get("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
         | 
| 24 | 
            +
              #
         | 
| 25 | 
            +
              # @example Update a webhook
         | 
| 26 | 
            +
              #   Resend::Webhooks.update(
         | 
| 27 | 
            +
              #     webhook_id: "430eed87-632a-4ea6-90db-0aace67ec228",
         | 
| 28 | 
            +
              #     endpoint: "https://new-webhook.example.com/handler",
         | 
| 29 | 
            +
              #     events: ["email.sent", "email.delivered"],
         | 
| 30 | 
            +
              #     status: "enabled"
         | 
| 31 | 
            +
              #   )
         | 
| 32 | 
            +
              #
         | 
| 33 | 
            +
              # @example Delete a webhook
         | 
| 34 | 
            +
              #   Resend::Webhooks.remove("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
         | 
| 35 | 
            +
              module Webhooks
         | 
| 36 | 
            +
                class << self
         | 
| 37 | 
            +
                  # Create a new webhook to receive real-time notifications about email events
         | 
| 38 | 
            +
                  #
         | 
| 39 | 
            +
                  # @param params [Hash] The webhook parameters
         | 
| 40 | 
            +
                  # @option params [String] :endpoint The URL where webhook events will be sent (required)
         | 
| 41 | 
            +
                  # @option params [Array<String>] :events Array of event types to subscribe to (required)
         | 
| 42 | 
            +
                  #
         | 
| 43 | 
            +
                  # @return [Hash] The webhook object containing id, object type, and signing_secret
         | 
| 44 | 
            +
                  #
         | 
| 45 | 
            +
                  # @example
         | 
| 46 | 
            +
                  #   Resend::Webhooks.create(
         | 
| 47 | 
            +
                  #     endpoint: "https://webhook.example.com/handler",
         | 
| 48 | 
            +
                  #     events: ["email.sent", "email.delivered", "email.bounced"]
         | 
| 49 | 
            +
                  #   )
         | 
| 50 | 
            +
                  def create(params = {})
         | 
| 51 | 
            +
                    path = "webhooks"
         | 
| 52 | 
            +
                    Resend::Request.new(path, params, "post").perform
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  # Retrieve a list of webhooks for the authenticated user
         | 
| 56 | 
            +
                  #
         | 
| 57 | 
            +
                  # @param params [Hash] The pagination parameters
         | 
| 58 | 
            +
                  # @option params [Integer] :limit Number of webhooks to retrieve (max: 100, min: 1)
         | 
| 59 | 
            +
                  # @option params [String] :after The ID after which to retrieve more webhooks (for pagination)
         | 
| 60 | 
            +
                  # @option params [String] :before The ID before which to retrieve more webhooks (for pagination)
         | 
| 61 | 
            +
                  #
         | 
| 62 | 
            +
                  # @return [Hash] A paginated list of webhook objects
         | 
| 63 | 
            +
                  #
         | 
| 64 | 
            +
                  # @example
         | 
| 65 | 
            +
                  #   Resend::Webhooks.list
         | 
| 66 | 
            +
                  #
         | 
| 67 | 
            +
                  # @example With pagination
         | 
| 68 | 
            +
                  #   Resend::Webhooks.list(limit: 20, after: "4dd369bc-aa82-4ff3-97de-514ae3000ee0")
         | 
| 69 | 
            +
                  def list(params = {})
         | 
| 70 | 
            +
                    path = Resend::PaginationHelper.build_paginated_path("webhooks", params)
         | 
| 71 | 
            +
                    Resend::Request.new(path, {}, "get").perform
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  # Retrieve a single webhook for the authenticated user
         | 
| 75 | 
            +
                  #
         | 
| 76 | 
            +
                  # @param webhook_id [String] The webhook ID
         | 
| 77 | 
            +
                  #
         | 
| 78 | 
            +
                  # @return [Hash] The webhook object with full details
         | 
| 79 | 
            +
                  #
         | 
| 80 | 
            +
                  # @example
         | 
| 81 | 
            +
                  #   Resend::Webhooks.get("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
         | 
| 82 | 
            +
                  def get(webhook_id = "")
         | 
| 83 | 
            +
                    path = "webhooks/#{webhook_id}"
         | 
| 84 | 
            +
                    Resend::Request.new(path, {}, "get").perform
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  # Update an existing webhook configuration
         | 
| 88 | 
            +
                  #
         | 
| 89 | 
            +
                  # @param params [Hash] The webhook update parameters
         | 
| 90 | 
            +
                  # @option params [String] :webhook_id The webhook ID (required)
         | 
| 91 | 
            +
                  # @option params [String] :endpoint The URL where webhook events will be sent
         | 
| 92 | 
            +
                  # @option params [Array<String>] :events Array of event types to subscribe to
         | 
| 93 | 
            +
                  # @option params [String] :status The webhook status ("enabled" or "disabled")
         | 
| 94 | 
            +
                  #
         | 
| 95 | 
            +
                  # @return [Hash] The updated webhook object
         | 
| 96 | 
            +
                  #
         | 
| 97 | 
            +
                  # @example
         | 
| 98 | 
            +
                  #   Resend::Webhooks.update(
         | 
| 99 | 
            +
                  #     webhook_id: "430eed87-632a-4ea6-90db-0aace67ec228",
         | 
| 100 | 
            +
                  #     endpoint: "https://new-webhook.example.com/handler",
         | 
| 101 | 
            +
                  #     events: ["email.sent", "email.delivered"],
         | 
| 102 | 
            +
                  #     status: "enabled"
         | 
| 103 | 
            +
                  #   )
         | 
| 104 | 
            +
                  def update(params = {})
         | 
| 105 | 
            +
                    webhook_id = params.delete(:webhook_id)
         | 
| 106 | 
            +
                    path = "webhooks/#{webhook_id}"
         | 
| 107 | 
            +
                    Resend::Request.new(path, params, "patch").perform
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  # Remove an existing webhook
         | 
| 111 | 
            +
                  #
         | 
| 112 | 
            +
                  # @param webhook_id [String] The webhook ID
         | 
| 113 | 
            +
                  #
         | 
| 114 | 
            +
                  # @return [Hash] Confirmation object with id, object type, and deleted status
         | 
| 115 | 
            +
                  #
         | 
| 116 | 
            +
                  # @example
         | 
| 117 | 
            +
                  #   Resend::Webhooks.remove("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
         | 
| 118 | 
            +
                  def remove(webhook_id = "")
         | 
| 119 | 
            +
                    path = "webhooks/#{webhook_id}"
         | 
| 120 | 
            +
                    Resend::Request.new(path, {}, "delete").perform
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  # Verify a webhook payload using HMAC-SHA256 signature validation
         | 
| 124 | 
            +
                  # This validates that the webhook request came from Resend and hasn't been tampered with
         | 
| 125 | 
            +
                  #
         | 
| 126 | 
            +
                  # @param params [Hash] The webhook verification parameters
         | 
| 127 | 
            +
                  # @option params [String] :payload The raw webhook payload body (required)
         | 
| 128 | 
            +
                  # @option params [Hash] :headers The webhook headers containing svix-id, svix-timestamp,
         | 
| 129 | 
            +
                  #   and svix-signature (required)
         | 
| 130 | 
            +
                  # @option params [String] :webhook_secret The signing secret from webhook creation (required)
         | 
| 131 | 
            +
                  #
         | 
| 132 | 
            +
                  # @return [Boolean] true if verification succeeds
         | 
| 133 | 
            +
                  # @raise [StandardError] If verification fails or required parameters are missing
         | 
| 134 | 
            +
                  #
         | 
| 135 | 
            +
                  # @example
         | 
| 136 | 
            +
                  #   Resend::Webhooks.verify(
         | 
| 137 | 
            +
                  #     payload: request.body.read,
         | 
| 138 | 
            +
                  #     headers: {
         | 
| 139 | 
            +
                  #       svix_id: "id_1234567890abcdefghijklmnopqrstuvwxyz",
         | 
| 140 | 
            +
                  #       svix_timestamp: "1616161616",
         | 
| 141 | 
            +
                  #       svix_signature: "v1,signature_here"
         | 
| 142 | 
            +
                  #     },
         | 
| 143 | 
            +
                  #     webhook_secret: "whsec_1234567890abcdez"
         | 
| 144 | 
            +
                  #   )
         | 
| 145 | 
            +
                  def verify(params = {})
         | 
| 146 | 
            +
                    payload = params[:payload]
         | 
| 147 | 
            +
                    headers = params[:headers] || {}
         | 
| 148 | 
            +
                    webhook_secret = params[:webhook_secret]
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                    validate_required_params(payload, headers, webhook_secret)
         | 
| 151 | 
            +
                    validate_timestamp(headers[:svix_timestamp])
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                    signed_content = "#{headers[:svix_id]}.#{headers[:svix_timestamp]}.#{payload}"
         | 
| 154 | 
            +
                    decoded_secret = decode_secret(webhook_secret)
         | 
| 155 | 
            +
                    expected_signature = generate_signature(decoded_secret, signed_content)
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    verify_signature(headers[:svix_signature], expected_signature)
         | 
| 158 | 
            +
                  end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                  private
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  # Validate required parameters
         | 
| 163 | 
            +
                  def validate_required_params(payload, headers, webhook_secret)
         | 
| 164 | 
            +
                    validate_payload(payload)
         | 
| 165 | 
            +
                    validate_webhook_secret(webhook_secret)
         | 
| 166 | 
            +
                    validate_headers(headers)
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  # Validate payload is present
         | 
| 170 | 
            +
                  def validate_payload(payload)
         | 
| 171 | 
            +
                    raise "payload cannot be empty" if payload.nil? || payload.empty?
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  # Validate webhook secret is present
         | 
| 175 | 
            +
                  def validate_webhook_secret(webhook_secret)
         | 
| 176 | 
            +
                    raise "webhook_secret cannot be empty" if webhook_secret.nil? || webhook_secret.empty?
         | 
| 177 | 
            +
                  end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  # Validate required headers are present
         | 
| 180 | 
            +
                  def validate_headers(headers)
         | 
| 181 | 
            +
                    raise "svix-id header is required" if headers[:svix_id].nil? || headers[:svix_id].empty?
         | 
| 182 | 
            +
                    raise "svix-timestamp header is required" if headers[:svix_timestamp].nil? || headers[:svix_timestamp].empty?
         | 
| 183 | 
            +
                    raise "svix-signature header is required" if headers[:svix_signature].nil? || headers[:svix_signature].empty?
         | 
| 184 | 
            +
                  end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                  # Validate timestamp to prevent replay attacks
         | 
| 187 | 
            +
                  def validate_timestamp(timestamp_header)
         | 
| 188 | 
            +
                    timestamp = timestamp_header.to_i
         | 
| 189 | 
            +
                    now = Time.now.to_i
         | 
| 190 | 
            +
                    diff = now - timestamp
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                    return unless diff > WEBHOOK_TOLERANCE_SECONDS || diff < -WEBHOOK_TOLERANCE_SECONDS
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                    raise "Timestamp outside tolerance window: difference of #{diff} seconds"
         | 
| 195 | 
            +
                  end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  # Decode the signing secret (strip whsec_ prefix and base64 decode)
         | 
| 198 | 
            +
                  def decode_secret(webhook_secret)
         | 
| 199 | 
            +
                    secret = webhook_secret.sub(/^whsec_/, "")
         | 
| 200 | 
            +
                    Base64.strict_decode64(secret)
         | 
| 201 | 
            +
                  rescue ArgumentError => e
         | 
| 202 | 
            +
                    raise "Failed to decode webhook secret: #{e.message}"
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                  # Verify signature using constant-time comparison
         | 
| 206 | 
            +
                  def verify_signature(signature_header, expected_signature)
         | 
| 207 | 
            +
                    signatures = signature_header.split(" ")
         | 
| 208 | 
            +
                    signatures.each do |sig|
         | 
| 209 | 
            +
                      parts = sig.split(",", 2)
         | 
| 210 | 
            +
                      next if parts.length != 2
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                      received_signature = parts[1]
         | 
| 213 | 
            +
                      return true if secure_compare(expected_signature, received_signature)
         | 
| 214 | 
            +
                    end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                    raise "No matching signature found"
         | 
| 217 | 
            +
                  end
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                  # Generate HMAC-SHA256 signature and return it as base64
         | 
| 220 | 
            +
                  def generate_signature(secret, content)
         | 
| 221 | 
            +
                    digest = OpenSSL::HMAC.digest("sha256", secret, content)
         | 
| 222 | 
            +
                    Base64.strict_encode64(digest)
         | 
| 223 | 
            +
                  end
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                  # Constant-time string comparison to prevent timing attacks
         | 
| 226 | 
            +
                  #
         | 
| 227 | 
            +
                  # Note: We implement this manually for Ruby 2.7 compatibility.
         | 
| 228 | 
            +
                  # Ruby 3.0+ could use OpenSSL.fixed_length_secure_compare instead.
         | 
| 229 | 
            +
                  def secure_compare(str_a, str_b)
         | 
| 230 | 
            +
                    return false if str_a.nil? || str_b.nil? || str_a.bytesize != str_b.bytesize
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                    bytes_a = str_a.unpack("C*")
         | 
| 233 | 
            +
                    result = 0
         | 
| 234 | 
            +
                    str_b.each_byte.with_index { |byte_b, i| result |= byte_b ^ bytes_a[i] }
         | 
| 235 | 
            +
                    result.zero?
         | 
| 236 | 
            +
                  end
         | 
| 237 | 
            +
                end
         | 
| 238 | 
            +
              end
         | 
| 239 | 
            +
            end
         | 
    
        data/lib/resend.rb
    CHANGED
    
    | @@ -20,9 +20,12 @@ require "resend/batch" | |
| 20 20 | 
             
            require "resend/contacts"
         | 
| 21 21 | 
             
            require "resend/domains"
         | 
| 22 22 | 
             
            require "resend/emails"
         | 
| 23 | 
            +
            require "resend/templates"
         | 
| 23 24 | 
             
            require "resend/emails/receiving"
         | 
| 24 | 
            -
            require "resend/attachments | 
| 25 | 
            +
            require "resend/emails/attachments"
         | 
| 26 | 
            +
            require "resend/emails/receiving/attachments"
         | 
| 25 27 | 
             
            require "resend/topics"
         | 
| 28 | 
            +
            require "resend/webhooks"
         | 
| 26 29 |  | 
| 27 30 | 
             
            # Rails
         | 
| 28 31 | 
             
            require "resend/railtie" if defined?(Rails) && defined?(ActionMailer)
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: resend
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.27.0.alpha. | 
| 4 | 
            +
              version: 0.27.0.alpha.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Derich Pacheco
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2025-10- | 
| 11 | 
            +
            date: 2025-10-28 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: httparty
         | 
| @@ -47,7 +47,6 @@ files: | |
| 47 47 | 
             
            - README.md
         | 
| 48 48 | 
             
            - lib/resend.rb
         | 
| 49 49 | 
             
            - lib/resend/api_keys.rb
         | 
| 50 | 
            -
            - lib/resend/attachments/receiving.rb
         | 
| 51 50 | 
             
            - lib/resend/audiences.rb
         | 
| 52 51 | 
             
            - lib/resend/batch.rb
         | 
| 53 52 | 
             
            - lib/resend/broadcasts.rb
         | 
| @@ -55,14 +54,18 @@ files: | |
| 55 54 | 
             
            - lib/resend/contacts.rb
         | 
| 56 55 | 
             
            - lib/resend/domains.rb
         | 
| 57 56 | 
             
            - lib/resend/emails.rb
         | 
| 57 | 
            +
            - lib/resend/emails/attachments.rb
         | 
| 58 58 | 
             
            - lib/resend/emails/receiving.rb
         | 
| 59 | 
            +
            - lib/resend/emails/receiving/attachments.rb
         | 
| 59 60 | 
             
            - lib/resend/errors.rb
         | 
| 60 61 | 
             
            - lib/resend/mailer.rb
         | 
| 61 62 | 
             
            - lib/resend/pagination_helper.rb
         | 
| 62 63 | 
             
            - lib/resend/railtie.rb
         | 
| 63 64 | 
             
            - lib/resend/request.rb
         | 
| 65 | 
            +
            - lib/resend/templates.rb
         | 
| 64 66 | 
             
            - lib/resend/topics.rb
         | 
| 65 67 | 
             
            - lib/resend/version.rb
         | 
| 68 | 
            +
            - lib/resend/webhooks.rb
         | 
| 66 69 | 
             
            homepage: https://github.com/resend/resend-ruby
         | 
| 67 70 | 
             
            licenses:
         | 
| 68 71 | 
             
            - MIT
         |