sevk 1.0.0 → 1.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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require 'openssl'
6
+
7
+ module Sevk
8
+ class WebhookVerificationError < StandardError; end
9
+
10
+ # Verifier for sevk webhook deliveries.
11
+ #
12
+ # sevk uses the Svix-compatible signature scheme (svix-id, svix-timestamp,
13
+ # svix-signature headers), so this helper or the official `svix` gem can be
14
+ # used. sevk issues canonical whsec_ base64 secrets that can be passed
15
+ # directly to either verifier.
16
+ class WebhookVerifier
17
+ TOLERANCE_SECONDS = 5 * 60
18
+
19
+ # Prepares a verifier with the signing secret issued for a webhook.
20
+ def initialize(secret)
21
+ raise WebhookVerificationError, 'Webhook secret is required' if secret.nil? || secret.empty?
22
+
23
+ @key = if secret.start_with?('whsec_')
24
+ begin
25
+ decoded = Base64.strict_decode64(secret[6..])
26
+ raise ArgumentError if decoded.empty?
27
+
28
+ decoded
29
+ rescue ArgumentError
30
+ raise WebhookVerificationError, 'Invalid base64 secret'
31
+ end
32
+ else
33
+ secret
34
+ end
35
+ end
36
+
37
+ # Verifies the delivery signature and timestamp, then parses its JSON payload.
38
+ def verify(payload, headers)
39
+ msg_id = lookup(headers, 'svix-id')
40
+ msg_timestamp = lookup(headers, 'svix-timestamp')
41
+ msg_signature = lookup(headers, 'svix-signature')
42
+
43
+ if msg_id.nil? || msg_id.empty? || msg_timestamp.nil? || msg_signature.nil?
44
+ raise WebhookVerificationError, 'Missing required svix headers'
45
+ end
46
+
47
+ raise WebhookVerificationError, 'Invalid timestamp header' unless msg_timestamp.match?(/\A[0-9]+\z/)
48
+
49
+ timestamp = Integer(msg_timestamp, 10)
50
+ now = Time.now.to_i
51
+ if timestamp < now - TOLERANCE_SECONDS || timestamp > now + TOLERANCE_SECONDS
52
+ raise WebhookVerificationError, 'Timestamp outside tolerance window'
53
+ end
54
+
55
+ to_sign = "#{msg_id}.#{msg_timestamp}.#{payload}"
56
+ digest = OpenSSL::HMAC.digest('SHA256', @key, to_sign)
57
+ expected = "v1,#{Base64.strict_encode64(digest)}"
58
+
59
+ msg_signature.split.each do |candidate|
60
+ next unless secure_compare(expected, candidate)
61
+
62
+ begin
63
+ return JSON.parse(payload)
64
+ rescue JSON::ParserError
65
+ raise WebhookVerificationError, 'Invalid JSON payload'
66
+ end
67
+ end
68
+
69
+ raise WebhookVerificationError, 'Signature mismatch'
70
+ rescue ArgumentError
71
+ raise WebhookVerificationError, 'Invalid timestamp header'
72
+ end
73
+
74
+ private
75
+
76
+ def lookup(headers, name)
77
+ headers.each do |key, value|
78
+ normalized = key.to_s.downcase.tr('_', '-').sub(/\Ahttp-/, '')
79
+ return value unless value.nil? || normalized != name
80
+ end
81
+
82
+ nil
83
+ end
84
+
85
+ def secure_compare(expected, candidate)
86
+ return false unless expected.bytesize == candidate.bytesize
87
+
88
+ OpenSSL.fixed_length_secure_compare(expected, candidate)
89
+ end
90
+ end
91
+ end
data/lib/sevk.rb CHANGED
@@ -1,28 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "faraday"
4
- require "faraday/retry"
5
- require "json"
6
-
7
- require_relative "sevk/version"
8
- require_relative "sevk/error"
9
- require_relative "sevk/client"
10
- require_relative "sevk/resources/base"
11
- require_relative "sevk/resources/contacts"
12
- require_relative "sevk/resources/audiences"
13
- require_relative "sevk/resources/templates"
14
- require_relative "sevk/resources/broadcasts"
15
- require_relative "sevk/resources/domains"
16
- require_relative "sevk/resources/topics"
17
- require_relative "sevk/resources/segments"
18
- require_relative "sevk/resources/subscriptions"
19
- require_relative "sevk/resources/emails"
20
- require_relative "sevk/resources/webhooks"
21
- require_relative "sevk/resources/events"
22
- require_relative "sevk/markup/renderer"
3
+ require 'faraday'
4
+ require 'json'
5
+
6
+ require_relative 'sevk/version'
7
+ require_relative 'sevk/error'
8
+ require_relative 'sevk/client'
9
+ require_relative 'sevk/resources/base'
10
+ require_relative 'sevk/resources/contacts'
11
+ require_relative 'sevk/resources/audiences'
12
+ require_relative 'sevk/resources/templates'
13
+ require_relative 'sevk/resources/broadcasts'
14
+ require_relative 'sevk/resources/domains'
15
+ require_relative 'sevk/resources/topics'
16
+ require_relative 'sevk/resources/segments'
17
+ require_relative 'sevk/resources/subscriptions'
18
+ require_relative 'sevk/resources/emails'
19
+ require_relative 'sevk/resources/webhooks'
20
+ require_relative 'sevk/resources/activity'
21
+ require_relative 'sevk/resources/inbound'
22
+ require_relative 'sevk/resources/outbound'
23
+ require_relative 'sevk/resources/usage'
24
+ require_relative 'sevk/webhook_verifier'
25
+ require_relative 'sevk/markup/renderer'
23
26
 
24
27
  module Sevk
25
- DEFAULT_BASE_URL = "https://api.sevk.io"
28
+ DEFAULT_BASE_URL = 'https://api.sevk.io'
26
29
 
27
30
  class << self
28
31
  attr_accessor :api_key, :base_url
@@ -39,221 +42,4 @@ module Sevk
39
42
  @client = nil
40
43
  end
41
44
  end
42
-
43
- # Class-based API
44
- module Contacts
45
- class << self
46
- def list(params = {})
47
- Sevk.client.contacts.list(**params)
48
- end
49
-
50
- def get(id)
51
- Sevk.client.contacts.get(id)
52
- end
53
-
54
- def create(params)
55
- Sevk.client.contacts.create(**params)
56
- end
57
-
58
- def update(id, params)
59
- Sevk.client.contacts.update(id, **params)
60
- end
61
-
62
- def delete(id)
63
- Sevk.client.contacts.delete(id)
64
- end
65
- end
66
- end
67
-
68
- module Audiences
69
- class << self
70
- def list(params = {})
71
- Sevk.client.audiences.list(**params)
72
- end
73
-
74
- def get(id)
75
- Sevk.client.audiences.get(id)
76
- end
77
-
78
- def create(params)
79
- Sevk.client.audiences.create(**params)
80
- end
81
-
82
- def update(id, params)
83
- Sevk.client.audiences.update(id, **params)
84
- end
85
-
86
- def delete(id)
87
- Sevk.client.audiences.delete(id)
88
- end
89
-
90
- def add_contacts(id, contact_ids)
91
- Sevk.client.audiences.add_contacts(id, contact_ids)
92
- end
93
- end
94
- end
95
-
96
- module Templates
97
- class << self
98
- def list(params = {})
99
- Sevk.client.templates.list(**params)
100
- end
101
-
102
- def get(id)
103
- Sevk.client.templates.get(id)
104
- end
105
-
106
- def create(params)
107
- Sevk.client.templates.create(**params)
108
- end
109
-
110
- def update(id, params)
111
- Sevk.client.templates.update(id, **params)
112
- end
113
-
114
- def delete(id)
115
- Sevk.client.templates.delete(id)
116
- end
117
-
118
- def duplicate(id)
119
- Sevk.client.templates.duplicate(id)
120
- end
121
- end
122
- end
123
-
124
- module Broadcasts
125
- class << self
126
- def list(params = {})
127
- Sevk.client.broadcasts.list(**params)
128
- end
129
-
130
- def get(id)
131
- Sevk.client.broadcasts.get(id)
132
- end
133
- end
134
- end
135
-
136
- module Domains
137
- class << self
138
- def list(params = {})
139
- Sevk.client.domains.list(**params)
140
- end
141
-
142
- def get(id)
143
- Sevk.client.domains.get(id)
144
- end
145
- end
146
- end
147
-
148
- module Topics
149
- class << self
150
- def list(audience_id, params = {})
151
- Sevk.client.topics.list(audience_id, **params)
152
- end
153
-
154
- def get(audience_id, topic_id)
155
- Sevk.client.topics.get(audience_id, topic_id)
156
- end
157
-
158
- def create(audience_id, params)
159
- Sevk.client.topics.create(audience_id, **params)
160
- end
161
-
162
- def update(audience_id, topic_id, params)
163
- Sevk.client.topics.update(audience_id, topic_id, **params)
164
- end
165
-
166
- def delete(audience_id, topic_id)
167
- Sevk.client.topics.delete(audience_id, topic_id)
168
- end
169
- end
170
- end
171
-
172
- module Segments
173
- class << self
174
- def list(audience_id, params = {})
175
- Sevk.client.segments.list(audience_id, **params)
176
- end
177
-
178
- def get(audience_id, segment_id)
179
- Sevk.client.segments.get(audience_id, segment_id)
180
- end
181
-
182
- def create(audience_id, params)
183
- Sevk.client.segments.create(audience_id, **params)
184
- end
185
-
186
- def update(audience_id, segment_id, params)
187
- Sevk.client.segments.update(audience_id, segment_id, **params)
188
- end
189
-
190
- def delete(audience_id, segment_id)
191
- Sevk.client.segments.delete(audience_id, segment_id)
192
- end
193
- end
194
- end
195
-
196
- module Subscriptions
197
- class << self
198
- def subscribe(params)
199
- Sevk.client.subscriptions.subscribe(**params)
200
- end
201
-
202
- def unsubscribe(params)
203
- Sevk.client.subscriptions.unsubscribe(**params)
204
- end
205
- end
206
- end
207
-
208
- module Emails
209
- class << self
210
- def send(params)
211
- Sevk.client.emails.send(**params)
212
- end
213
- end
214
- end
215
-
216
- module Webhooks
217
- class << self
218
- def list(params = {})
219
- Sevk.client.webhooks.list(**params)
220
- end
221
-
222
- def get(id)
223
- Sevk.client.webhooks.get(id)
224
- end
225
-
226
- def create(params)
227
- Sevk.client.webhooks.create(params)
228
- end
229
-
230
- def update(id, params)
231
- Sevk.client.webhooks.update(id, params)
232
- end
233
-
234
- def delete(id)
235
- Sevk.client.webhooks.delete(id)
236
- end
237
-
238
- def test(id)
239
- Sevk.client.webhooks.test(id)
240
- end
241
-
242
- def list_events
243
- Sevk.client.webhooks.list_events
244
- end
245
- end
246
- end
247
-
248
- module Events
249
- class << self
250
- def list(params = {})
251
- Sevk.client.events.list(**params)
252
- end
253
-
254
- def stats
255
- Sevk.client.events.stats
256
- end
257
- end
258
- end
259
45
  end
metadata CHANGED
@@ -1,30 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sevk
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: faraday
13
+ name: base64
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '2.0'
18
+ version: '0.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '2.0'
25
+ version: '0.2'
26
26
  - !ruby/object:Gem::Dependency
27
- name: faraday-retry
27
+ name: faraday
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
@@ -37,20 +37,6 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
- - !ruby/object:Gem::Dependency
41
- name: nokogiri
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '1.15'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.15'
54
40
  - !ruby/object:Gem::Dependency
55
41
  name: rouge
56
42
  requirement: !ruby/object:Gem::Requirement
@@ -65,76 +51,6 @@ dependencies:
65
51
  - - "~>"
66
52
  - !ruby/object:Gem::Version
67
53
  version: '4.0'
68
- - !ruby/object:Gem::Dependency
69
- name: bundler
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '2.0'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '2.0'
82
- - !ruby/object:Gem::Dependency
83
- name: rake
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '13.0'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '13.0'
96
- - !ruby/object:Gem::Dependency
97
- name: rspec
98
- requirement: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '3.0'
103
- type: :development
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '3.0'
110
- - !ruby/object:Gem::Dependency
111
- name: webmock
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: '3.0'
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: '3.0'
124
- - !ruby/object:Gem::Dependency
125
- name: rubocop
126
- requirement: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - "~>"
129
- - !ruby/object:Gem::Version
130
- version: '1.0'
131
- type: :development
132
- prerelease: false
133
- version_requirements: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '1.0'
138
54
  description: Ruby SDK for interacting with the Sevk API. Send emails, manage contacts,
139
55
  audiences, templates, and more.
140
56
  email:
@@ -143,33 +59,44 @@ executables: []
143
59
  extensions: []
144
60
  extra_rdoc_files: []
145
61
  files:
62
+ - LICENSE
146
63
  - README.md
147
- - Rakefile
148
64
  - lib/sevk.rb
149
65
  - lib/sevk/client.rb
150
66
  - lib/sevk/error.rb
67
+ - lib/sevk/markup/document_parser.rb
68
+ - lib/sevk/markup/html_document.rb
69
+ - lib/sevk/markup/number_formatting.rb
70
+ - lib/sevk/markup/render_limits.rb
151
71
  - lib/sevk/markup/renderer.rb
72
+ - lib/sevk/markup/style_helpers.rb
73
+ - lib/sevk/markup/syntax_highlighting.rb
74
+ - lib/sevk/markup/tag_scanner.rb
75
+ - lib/sevk/markup/template_engine.rb
76
+ - lib/sevk/resources/activity.rb
152
77
  - lib/sevk/resources/audiences.rb
153
78
  - lib/sevk/resources/base.rb
154
79
  - lib/sevk/resources/broadcasts.rb
155
80
  - lib/sevk/resources/contacts.rb
156
81
  - lib/sevk/resources/domains.rb
157
82
  - lib/sevk/resources/emails.rb
158
- - lib/sevk/resources/events.rb
83
+ - lib/sevk/resources/inbound.rb
84
+ - lib/sevk/resources/outbound.rb
159
85
  - lib/sevk/resources/segments.rb
160
86
  - lib/sevk/resources/subscriptions.rb
161
87
  - lib/sevk/resources/templates.rb
162
88
  - lib/sevk/resources/topics.rb
89
+ - lib/sevk/resources/usage.rb
163
90
  - lib/sevk/resources/webhooks.rb
164
91
  - lib/sevk/version.rb
165
- - sevk.gemspec
92
+ - lib/sevk/webhook_verifier.rb
166
93
  homepage: https://sevk.io
167
94
  licenses:
168
95
  - MIT
169
96
  metadata:
170
97
  homepage_uri: https://sevk.io
171
98
  source_code_uri: https://github.com/sevk-io/sevk-ruby
172
- changelog_uri: https://github.com/sevk-io/sevk-ruby/blob/main/CHANGELOG.md
99
+ rubygems_mfa_required: 'true'
173
100
  rdoc_options: []
174
101
  require_paths:
175
102
  - lib
data/Rakefile DELETED
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
9
-
10
- desc "Update all dependencies to latest versions"
11
- task :update do
12
- sh "bundle update"
13
- end
14
-
15
- desc "Update dependencies and show outdated gems"
16
- task :outdated do
17
- sh "bundle outdated"
18
- end
19
-
20
- desc "Run tests"
21
- task :test => :spec
22
-
23
- desc "Build the gem"
24
- task :build do
25
- sh "gem build sevk.gemspec"
26
- end
27
-
28
- desc "Install the gem locally"
29
- task :install => :build do
30
- sh "gem install sevk-*.gem"
31
- end
32
-
33
- desc "Release the gem to RubyGems"
34
- task :release => :build do
35
- sh "gem push sevk-*.gem"
36
- end
37
-
38
- desc "Clean up build artifacts"
39
- task :clean do
40
- FileUtils.rm_f Dir.glob("sevk-*.gem")
41
- FileUtils.rm_rf "pkg"
42
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base"
4
-
5
- module Sevk
6
- module Resources
7
- class Events < Base
8
- def list(page: nil, limit: nil, type: nil, contact_id: nil)
9
- params = { page: page, limit: limit, type: type, contact_id: contact_id }
10
- client.get("/events#{build_query_string(params)}")
11
- end
12
-
13
- def stats
14
- client.get("/events/stats")
15
- end
16
- end
17
- end
18
- end
data/sevk.gemspec DELETED
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/sevk/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "sevk"
7
- spec.version = Sevk::VERSION
8
- spec.authors = ["Sevk"]
9
- spec.email = ["dev@sevk.io"]
10
-
11
- spec.summary = "Ruby SDK for Sevk API"
12
- spec.description = "Ruby SDK for interacting with the Sevk API. Send emails, manage contacts, audiences, templates, and more."
13
- spec.homepage = "https://sevk.io"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.0.0"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/sevk-io/sevk-ruby"
19
- spec.metadata["changelog_uri"] = "https://github.com/sevk-io/sevk-ruby/blob/main/CHANGELOG.md"
20
-
21
- spec.files = Dir.chdir(__dir__) do
22
- `git ls-files -z`.split("\x0").reject do |f|
23
- (File.expand_path(f) == __FILE__) ||
24
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
25
- end
26
- end
27
- spec.bindir = "exe"
28
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
- spec.require_paths = ["lib"]
30
-
31
- spec.add_dependency "faraday", "~> 2.0"
32
- spec.add_dependency "faraday-retry", "~> 2.0"
33
- spec.add_dependency "nokogiri", "~> 1.15"
34
- spec.add_dependency "rouge", "~> 4.0"
35
-
36
- spec.add_development_dependency "bundler", "~> 2.0"
37
- spec.add_development_dependency "rake", "~> 13.0"
38
- spec.add_development_dependency "rspec", "~> 3.0"
39
- spec.add_development_dependency "webmock", "~> 3.0"
40
- spec.add_development_dependency "rubocop", "~> 1.0"
41
- end