sevk 0.1.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,26 +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/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'
21
26
 
22
27
  module Sevk
23
- DEFAULT_BASE_URL = "https://api.sevk.io"
28
+ DEFAULT_BASE_URL = 'https://api.sevk.io'
24
29
 
25
30
  class << self
26
31
  attr_accessor :api_key, :base_url
@@ -37,177 +42,4 @@ module Sevk
37
42
  @client = nil
38
43
  end
39
44
  end
40
-
41
- # Class-based API
42
- module Contacts
43
- class << self
44
- def list(params = {})
45
- Sevk.client.contacts.list(**params)
46
- end
47
-
48
- def get(id)
49
- Sevk.client.contacts.get(id)
50
- end
51
-
52
- def create(params)
53
- Sevk.client.contacts.create(**params)
54
- end
55
-
56
- def update(id, params)
57
- Sevk.client.contacts.update(id, **params)
58
- end
59
-
60
- def delete(id)
61
- Sevk.client.contacts.delete(id)
62
- end
63
- end
64
- end
65
-
66
- module Audiences
67
- class << self
68
- def list(params = {})
69
- Sevk.client.audiences.list(**params)
70
- end
71
-
72
- def get(id)
73
- Sevk.client.audiences.get(id)
74
- end
75
-
76
- def create(params)
77
- Sevk.client.audiences.create(**params)
78
- end
79
-
80
- def update(id, params)
81
- Sevk.client.audiences.update(id, **params)
82
- end
83
-
84
- def delete(id)
85
- Sevk.client.audiences.delete(id)
86
- end
87
-
88
- def add_contacts(id, contact_ids)
89
- Sevk.client.audiences.add_contacts(id, contact_ids)
90
- end
91
- end
92
- end
93
-
94
- module Templates
95
- class << self
96
- def list(params = {})
97
- Sevk.client.templates.list(**params)
98
- end
99
-
100
- def get(id)
101
- Sevk.client.templates.get(id)
102
- end
103
-
104
- def create(params)
105
- Sevk.client.templates.create(**params)
106
- end
107
-
108
- def update(id, params)
109
- Sevk.client.templates.update(id, **params)
110
- end
111
-
112
- def delete(id)
113
- Sevk.client.templates.delete(id)
114
- end
115
-
116
- def duplicate(id)
117
- Sevk.client.templates.duplicate(id)
118
- end
119
- end
120
- end
121
-
122
- module Broadcasts
123
- class << self
124
- def list(params = {})
125
- Sevk.client.broadcasts.list(**params)
126
- end
127
-
128
- def get(id)
129
- Sevk.client.broadcasts.get(id)
130
- end
131
- end
132
- end
133
-
134
- module Domains
135
- class << self
136
- def list(params = {})
137
- Sevk.client.domains.list(**params)
138
- end
139
-
140
- def get(id)
141
- Sevk.client.domains.get(id)
142
- end
143
- end
144
- end
145
-
146
- module Topics
147
- class << self
148
- def list(audience_id, params = {})
149
- Sevk.client.topics.list(audience_id, **params)
150
- end
151
-
152
- def get(audience_id, topic_id)
153
- Sevk.client.topics.get(audience_id, topic_id)
154
- end
155
-
156
- def create(audience_id, params)
157
- Sevk.client.topics.create(audience_id, **params)
158
- end
159
-
160
- def update(audience_id, topic_id, params)
161
- Sevk.client.topics.update(audience_id, topic_id, **params)
162
- end
163
-
164
- def delete(audience_id, topic_id)
165
- Sevk.client.topics.delete(audience_id, topic_id)
166
- end
167
- end
168
- end
169
-
170
- module Segments
171
- class << self
172
- def list(audience_id, params = {})
173
- Sevk.client.segments.list(audience_id, **params)
174
- end
175
-
176
- def get(audience_id, segment_id)
177
- Sevk.client.segments.get(audience_id, segment_id)
178
- end
179
-
180
- def create(audience_id, params)
181
- Sevk.client.segments.create(audience_id, **params)
182
- end
183
-
184
- def update(audience_id, segment_id, params)
185
- Sevk.client.segments.update(audience_id, segment_id, **params)
186
- end
187
-
188
- def delete(audience_id, segment_id)
189
- Sevk.client.segments.delete(audience_id, segment_id)
190
- end
191
- end
192
- end
193
-
194
- module Subscriptions
195
- class << self
196
- def subscribe(params)
197
- Sevk.client.subscriptions.subscribe(**params)
198
- end
199
-
200
- def unsubscribe(params)
201
- Sevk.client.subscriptions.unsubscribe(**params)
202
- end
203
- end
204
- end
205
-
206
- module Emails
207
- class << self
208
- def send(params)
209
- Sevk.client.emails.send(**params)
210
- end
211
- end
212
- end
213
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: 0.1.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
  - - "~>"
@@ -38,89 +38,19 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: nokogiri
41
+ name: rouge
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.15'
46
+ version: '4.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.15'
54
- - !ruby/object:Gem::Dependency
55
- name: bundler
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '2.0'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '2.0'
68
- - !ruby/object:Gem::Dependency
69
- name: rake
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '13.0'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '13.0'
82
- - !ruby/object:Gem::Dependency
83
- name: rspec
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '3.0'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '3.0'
96
- - !ruby/object:Gem::Dependency
97
- name: webmock
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: rubocop
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: '1.0'
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: '1.0'
53
+ version: '4.0'
124
54
  description: Ruby SDK for interacting with the Sevk API. Send emails, manage contacts,
125
55
  audiences, templates, and more.
126
56
  email:
@@ -129,31 +59,44 @@ executables: []
129
59
  extensions: []
130
60
  extra_rdoc_files: []
131
61
  files:
62
+ - LICENSE
132
63
  - README.md
133
- - Rakefile
134
64
  - lib/sevk.rb
135
65
  - lib/sevk/client.rb
136
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
137
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
138
77
  - lib/sevk/resources/audiences.rb
139
78
  - lib/sevk/resources/base.rb
140
79
  - lib/sevk/resources/broadcasts.rb
141
80
  - lib/sevk/resources/contacts.rb
142
81
  - lib/sevk/resources/domains.rb
143
82
  - lib/sevk/resources/emails.rb
83
+ - lib/sevk/resources/inbound.rb
84
+ - lib/sevk/resources/outbound.rb
144
85
  - lib/sevk/resources/segments.rb
145
86
  - lib/sevk/resources/subscriptions.rb
146
87
  - lib/sevk/resources/templates.rb
147
88
  - lib/sevk/resources/topics.rb
89
+ - lib/sevk/resources/usage.rb
90
+ - lib/sevk/resources/webhooks.rb
148
91
  - lib/sevk/version.rb
149
- - sevk.gemspec
92
+ - lib/sevk/webhook_verifier.rb
150
93
  homepage: https://sevk.io
151
94
  licenses:
152
95
  - MIT
153
96
  metadata:
154
97
  homepage_uri: https://sevk.io
155
98
  source_code_uri: https://github.com/sevk-io/sevk-ruby
156
- changelog_uri: https://github.com/sevk-io/sevk-ruby/blob/main/CHANGELOG.md
99
+ rubygems_mfa_required: 'true'
157
100
  rdoc_options: []
158
101
  require_paths:
159
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
data/sevk.gemspec DELETED
@@ -1,40 +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
-
35
- spec.add_development_dependency "bundler", "~> 2.0"
36
- spec.add_development_dependency "rake", "~> 13.0"
37
- spec.add_development_dependency "rspec", "~> 3.0"
38
- spec.add_development_dependency "webmock", "~> 3.0"
39
- spec.add_development_dependency "rubocop", "~> 1.0"
40
- end