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.
- checksums.yaml +7 -0
- data/README.md +313 -0
- data/lib/sendara/client.rb +240 -0
- data/lib/sendara/errors.rb +23 -0
- data/lib/sendara/generators/install_generator.rb +17 -0
- data/lib/sendara/generators/templates/sendara.rb +8 -0
- data/lib/sendara/message_page.rb +34 -0
- data/lib/sendara/railtie.rb +72 -0
- data/lib/sendara/resource.rb +26 -0
- data/lib/sendara/resources/api_keys.rb +28 -0
- data/lib/sendara/resources/billing.rb +22 -0
- data/lib/sendara/resources/broadcasts.rb +72 -0
- data/lib/sendara/resources/contacts.rb +121 -0
- data/lib/sendara/resources/domains.rb +44 -0
- data/lib/sendara/resources/emails.rb +49 -0
- data/lib/sendara/resources/lists.rb +56 -0
- data/lib/sendara/resources/messages.rb +42 -0
- data/lib/sendara/resources/suppressions.rb +29 -0
- data/lib/sendara/resources/templates.rb +54 -0
- data/lib/sendara/resources/usage.rb +12 -0
- data/lib/sendara/version.rb +5 -0
- data/lib/sendara/webhooks.rb +74 -0
- data/lib/sendara.rb +27 -0
- data/sendara.gemspec +30 -0
- metadata +86 -0
|
@@ -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: []
|