sendara 0.1.0

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.
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module Sendara
7
+ module Webhooks
8
+ SIGNATURE_HEADER = "Sendara-Signature"
9
+ TIMESTAMP_HEADER = "Sendara-Timestamp"
10
+ EVENT_ID_HEADER = "Sendara-Event-Id"
11
+ EVENT_TYPE_HEADER = "Sendara-Event-Type"
12
+
13
+ module_function
14
+
15
+ def verify(payload, headers, secret, tolerance: 300)
16
+ raise WebhookVerificationError, "A signing secret is required" if secret.nil? || secret.empty?
17
+
18
+ raw_body = payload.to_s
19
+ signature = header_value(headers, SIGNATURE_HEADER)
20
+ timestamp = header_value(headers, TIMESTAMP_HEADER)
21
+
22
+ raise WebhookVerificationError, "Missing #{SIGNATURE_HEADER} header" if signature.nil? || signature.empty?
23
+ raise WebhookVerificationError, "Missing #{TIMESTAMP_HEADER} header" if timestamp.nil? || timestamp.empty?
24
+
25
+ if tolerance.positive?
26
+ raise WebhookVerificationError, "Invalid timestamp header" unless timestamp.match?(/\A-?\d+\z/)
27
+
28
+ skew = (Time.now.to_i - timestamp.to_i).abs
29
+ raise WebhookVerificationError, "Timestamp outside tolerance window" if skew > tolerance
30
+ end
31
+
32
+ expected = sign(secret, timestamp, raw_body)
33
+ raise WebhookVerificationError, "Signature mismatch" unless secure_compare(expected, signature)
34
+
35
+ begin
36
+ JSON.parse(raw_body)
37
+ rescue JSON::ParserError
38
+ raise WebhookVerificationError, "Body is not valid JSON"
39
+ end
40
+ end
41
+
42
+ def sign(secret, timestamp, raw_body)
43
+ OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
44
+ end
45
+
46
+ def secure_compare(expected, provided)
47
+ expected_bytes = expected.to_s.b
48
+ provided_bytes = provided.to_s.b
49
+ return false unless expected_bytes.bytesize == provided_bytes.bytesize
50
+
51
+ OpenSSL.fixed_length_secure_compare(expected_bytes, provided_bytes)
52
+ end
53
+
54
+ def header_value(headers, name)
55
+ return nil if headers.nil?
56
+
57
+ if headers.respond_to?(:[]) && !headers.is_a?(Array)
58
+ direct = headers[name] || headers[name.downcase] || headers[name.upcase]
59
+ return scalar_header(direct) unless direct.nil?
60
+ end
61
+
62
+ lower = name.downcase
63
+ headers.each do |key, value|
64
+ return scalar_header(value) if key.to_s.downcase == lower
65
+ end
66
+ nil
67
+ end
68
+
69
+ def scalar_header(value)
70
+ value = value.first if value.is_a?(Array)
71
+ value.nil? ? nil : value.to_s
72
+ end
73
+ end
74
+ end
data/lib/sendara.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sendara/version"
4
+ require_relative "sendara/errors"
5
+ require_relative "sendara/message_page"
6
+ require_relative "sendara/resource"
7
+ require_relative "sendara/resources/emails"
8
+ require_relative "sendara/resources/broadcasts"
9
+ require_relative "sendara/resources/messages"
10
+ require_relative "sendara/resources/contacts"
11
+ require_relative "sendara/resources/lists"
12
+ require_relative "sendara/resources/domains"
13
+ require_relative "sendara/resources/templates"
14
+ require_relative "sendara/resources/suppressions"
15
+ require_relative "sendara/resources/usage"
16
+ require_relative "sendara/resources/api_keys"
17
+ require_relative "sendara/resources/billing"
18
+ require_relative "sendara/client"
19
+ require_relative "sendara/webhooks"
20
+
21
+ module Sendara
22
+ def self.new(api_key, **options)
23
+ Client.new(api_key, **options)
24
+ end
25
+ end
26
+
27
+ require_relative "sendara/railtie" if defined?(Rails::Railtie)
data/sendara.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sendara/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sendara"
7
+ spec.version = Sendara::VERSION
8
+ spec.authors = ["Sendara"]
9
+ spec.email = ["support@sendara.dev"]
10
+
11
+ spec.summary = "Sendara — email & marketing API client for Ruby."
12
+ spec.description = "Email-first Ruby client for the Sendara API: transactional email, " \
13
+ "broadcasts, contacts, templates, domains, and signed webhooks. " \
14
+ "Pure stdlib HTTP, zero runtime dependencies."
15
+ spec.homepage = "https://sendara.dev"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.0"
18
+
19
+ spec.metadata = {
20
+ "homepage_uri" => spec.homepage,
21
+ "source_code_uri" => "https://github.com/sendara/sendara-ruby",
22
+ "documentation_uri" => "https://sendara.dev/docs",
23
+ "rubygems_mfa_required" => "true"
24
+ }
25
+
26
+ spec.files = Dir.glob("lib/**/*.rb") + %w[README.md LICENSE sendara.gemspec].select { |f| File.exist?(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "rspec", "~> 3"
30
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sendara
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sendara
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ description: 'Email-first Ruby client for the Sendara API: transactional email, broadcasts,
28
+ contacts, templates, domains, and signed webhooks. Pure stdlib HTTP, zero runtime
29
+ dependencies.'
30
+ email:
31
+ - support@sendara.dev
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - README.md
37
+ - lib/sendara.rb
38
+ - lib/sendara/client.rb
39
+ - lib/sendara/errors.rb
40
+ - lib/sendara/generators/install_generator.rb
41
+ - lib/sendara/generators/templates/sendara.rb
42
+ - lib/sendara/message_page.rb
43
+ - lib/sendara/railtie.rb
44
+ - lib/sendara/resource.rb
45
+ - lib/sendara/resources/api_keys.rb
46
+ - lib/sendara/resources/billing.rb
47
+ - lib/sendara/resources/broadcasts.rb
48
+ - lib/sendara/resources/contacts.rb
49
+ - lib/sendara/resources/domains.rb
50
+ - lib/sendara/resources/emails.rb
51
+ - lib/sendara/resources/lists.rb
52
+ - lib/sendara/resources/messages.rb
53
+ - lib/sendara/resources/suppressions.rb
54
+ - lib/sendara/resources/templates.rb
55
+ - lib/sendara/resources/usage.rb
56
+ - lib/sendara/version.rb
57
+ - lib/sendara/webhooks.rb
58
+ - sendara.gemspec
59
+ homepage: https://sendara.dev
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://sendara.dev
64
+ source_code_uri: https://github.com/sendara/sendara-ruby
65
+ documentation_uri: https://sendara.dev/docs
66
+ rubygems_mfa_required: 'true'
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.5.22
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Sendara — email & marketing API client for Ruby.
86
+ test_files: []