ses-dashboard 0.1.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7165bdb98b1bf82aa3b6a65e7346101bc9792eb81e7b81fd8c1c9128865697f
4
- data.tar.gz: 520bc27a98c191b442b307bb7878cc4566883d2567db36373ea62396e867748b
3
+ metadata.gz: 58342ca28ca9d49b31764e988689df488d9a43061fdb145ded04a7d2dbb038b6
4
+ data.tar.gz: da07fcf3d2549fc68bdf24aceb72d289e3a8bf5d93238cd807932b71ef4d7cec
5
5
  SHA512:
6
- metadata.gz: c77bbac1e82db66182c2bd4141d8ab66ea9e9fb267dc0f33b0a082add3af7bf5ed72bdfb7c6bb2dd772d5db4ae19db61b120681d8f6d8001816ed90dbb8f7dfc
7
- data.tar.gz: 8e3f511a3be5108a96247ff6fb42cfa05aed17003637f9d43c92863b6629a653cfd71ce74f5606952750a0655018fbfb01c8d06eedfd8f89cfdd6dce7f2b12d9
6
+ metadata.gz: 8757e8e5024c11d93826bdb890e46dcf6fef0bac259d43da0dbec7bd64e16f07765b01967a2d7b5bbfa75de2d11f9bf193b1949f81c937b2e450fba6b2c13092
7
+ data.tar.gz: b222f9246a72fd94869c85e316a69668519536e8fc973e205036ce64ea92c02dd233a6268f889dde3a871759478a1a915e6379e4bed36845c41409acf5c2d208
data/README.md CHANGED
@@ -1,7 +1,44 @@
1
1
  # SES Dashboard
2
+ [![codecov](https://codecov.io/gh/antodoms/ses_dashboard/graph/badge.svg?token=0SSO12E12W)](https://codecov.io/gh/antodoms/ses_dashboard)
3
+ [![Gem Version](https://badge.fury.io/rb/ses-dashboard.svg)](https://badge.fury.io/rb/ses-dashboard)
2
4
 
3
5
  A mountable Rails engine that provides a real-time dashboard for Amazon SES, tracking email delivery, bounces, complaints, opens, and clicks via SNS webhooks.
4
6
 
7
+ ## Screenshots
8
+
9
+ ![Combined Dashboard](docs/screenshots/combined-projects-dashboard.png)
10
+
11
+ <table>
12
+ <tr>
13
+ <td><img src="docs/screenshots/project-dashboard-view-page.png" alt="Project Dashboard" /></td>
14
+ <td><img src="docs/screenshots/project-activity-page.png" alt="Activity Log" /></td>
15
+ </tr>
16
+ <tr>
17
+ <td align="center"><em>Per-project stats &amp; email volume chart</em></td>
18
+ <td align="center"><em>Paginated activity log with search &amp; export</em></td>
19
+ </tr>
20
+ <tr>
21
+ <td colspan="2"><img src="docs/screenshots/email-details.png" alt="Email Detail" /></td>
22
+ </tr>
23
+ <tr>
24
+ <td colspan="2" align="center"><em>Email detail with full SNS event timeline</em></td>
25
+ </tr>
26
+ </table>
27
+
28
+ ## Features
29
+
30
+ - **Real-time webhook processing** -- receives SNS notifications for delivery, bounce, complaint, open, click, reject, and rendering failure events
31
+ - **Per-project dashboards** -- stat cards, email volume charts (Chart.js), and paginated activity logs
32
+ - **Pluggable authentication** -- ships with Devise, Cloudflare Zero Trust, and no-auth adapters; bring your own with any object that responds to `#authenticate(request)`
33
+ - **CSV/JSON export** -- export filtered email activity from any project
34
+ - **Test email sending** -- send test emails directly from the dashboard via the SES API
35
+ - **Status state machine** -- unidirectional email status transitions (sent -> delivered/bounced/etc.)
36
+ - **SNS signature verification** -- validates RSA signatures (SHA1 and SHA256) on incoming SNS messages
37
+ - **Database agnostic** -- works with SQLite, PostgreSQL, and MySQL
38
+ - **Lightweight pagination** -- no external pagination gem required
39
+
40
+ ## Architecture
41
+
5
42
  ```mermaid
6
43
  graph TB
7
44
  subgraph Host["Host Rails App"]
@@ -29,6 +66,7 @@ graph TB
29
66
  subgraph Core["Core Library (lib/)"]
30
67
  Client["Client<br/>AWS SES SDK wrapper"]
31
68
  WP["WebhookProcessor<br/>SNS message parser"]
69
+ SV["SnsSignatureVerifier"]
32
70
  SA["StatsAggregator<br/>Dashboard statistics"]
33
71
  Pag["Paginatable"]
34
72
  end
@@ -54,7 +92,8 @@ graph TB
54
92
  DC --> SA
55
93
  EC --> Pag
56
94
  TC --> Client
57
- WC -->|"POST /webhook/:token"| WP
95
+ WC -->|"POST /webhook/:token"| SV
96
+ SV --> WP
58
97
  WP --> WEP
59
98
  WEP --> Models
60
99
  Client --> SES
@@ -68,31 +107,22 @@ graph TB
68
107
  style Host fill:#e8f5e9,stroke:#4caf50
69
108
  ```
70
109
 
71
- ## Features
72
-
73
- - **Real-time webhook processing** -- receives SNS notifications for delivery, bounce, complaint, open, click, reject, and rendering failure events
74
- - **Per-project dashboards** -- stat cards, email volume charts (Chart.js), and paginated activity logs
75
- - **Pluggable authentication** -- ships with Devise, Cloudflare Zero Trust, and no-auth adapters; bring your own with any object that responds to `#authenticate(request)`
76
- - **CSV/JSON export** -- export filtered email activity from any project
77
- - **Test email sending** -- send test emails directly from the dashboard via the SES API
78
- - **Status state machine** -- unidirectional email status transitions (sent -> delivered/bounced/etc.)
79
- - **Database agnostic** -- works with SQLite, PostgreSQL, and MySQL
80
- - **Lightweight pagination** -- no external pagination gem required
81
-
82
110
  ## Installation
83
111
 
84
112
  Add the gem to your Gemfile:
85
113
 
86
114
  ```ruby
87
- gem "ses_dashboard"
115
+ gem "ses-dashboard"
88
116
  ```
89
117
 
118
+ The gem name uses a hyphen (`ses-dashboard`) — Bundler will auto-require the correct entry point automatically.
119
+
90
120
  Then run:
91
121
 
92
122
  ```bash
93
123
  bundle install
94
- rails ses_dashboard:install:migrations
95
- rails db:migrate
124
+ bin/rails railties:install:migrations
125
+ bin/rails db:migrate
96
126
  ```
97
127
 
98
128
  ## Mounting
@@ -128,8 +158,8 @@ SesDashboard.configure do |c|
128
158
  c.cloudflare_aud = "your-application-aud"
129
159
 
130
160
  # Dashboard behaviour
131
- c.per_page = 25 # rows per page in the activity log
132
- c.time_zone = "UTC" # timezone for chart date grouping
161
+ c.per_page = 25 # rows per page in the activity log
162
+ c.time_zone = "UTC" # timezone for chart date grouping
133
163
  c.test_email_from = "noreply@example.com"
134
164
 
135
165
  # Caching & security
@@ -144,24 +174,55 @@ Every controller action (except the webhook endpoint) runs through the configure
144
174
 
145
175
  | Adapter | Value | Notes |
146
176
  |---|---|---|
147
- | None | `:none` | Open access -- suitable for development |
177
+ | None | `:none` | Open access suitable for development |
148
178
  | Devise | `:devise` | Calls `authenticate_user!` via Warden |
149
179
  | Cloudflare Zero Trust | `:cloudflare` | Validates `CF_Authorization` JWT against JWKS |
150
180
  | Custom | any object | Must respond to `#authenticate(request)` returning truthy/falsy |
151
181
 
182
+ ### Custom adapter
183
+
184
+ Use a custom adapter when your app has its own authentication system (e.g. custom session-based auth, API keys, JWT):
185
+
152
186
  ```ruby
153
- # Example custom adapter
154
- class ApiKeyAuth
187
+ # config/initializers/ses_dashboard.rb
188
+
189
+ my_auth = Class.new(SesDashboard::Auth::Base) do
155
190
  def authenticate(request)
156
- request.headers["X-Api-Key"] == Rails.application.credentials.dashboard_key
191
+ session = request.session
192
+ # your auth logic here — return truthy to allow, falsy to deny
193
+ session[:user_id].present?
157
194
  end
158
195
  end
159
196
 
160
197
  SesDashboard.configure do |c|
161
- c.authentication_adapter = ApiKeyAuth.new
198
+ c.authentication_adapter = my_auth.new
162
199
  end
163
200
  ```
164
201
 
202
+ For apps with session timeout and whitelist checks (e.g. custom Rails session auth):
203
+
204
+ ```ruby
205
+ my_auth = Class.new(SesDashboard::Auth::Base) do
206
+ def authenticate(request)
207
+ session = request.session
208
+ user_id = session[:user_id]
209
+ logged_in_at = session[:logged_in_at]
210
+
211
+ return false unless user_id && logged_in_at
212
+ return false unless logged_in_at > 12.hours.ago
213
+
214
+ user = User.find_by(id: user_id)
215
+ user&.active? || false
216
+ end
217
+ end
218
+
219
+ SesDashboard.configure do |c|
220
+ c.authentication_adapter = my_auth.new
221
+ end
222
+ ```
223
+
224
+ The adapter is defined inline using `Class.new` so it is available at initializer load time without depending on Zeitwerk autoloading.
225
+
165
226
  ## SNS Webhook Setup
166
227
 
167
228
  Each project gets a unique webhook URL displayed on its dashboard page:
@@ -178,17 +239,17 @@ To connect it to SES:
178
239
 
179
240
  The webhook endpoint authenticates via the project token in the URL and does not require a session.
180
241
 
181
- ## Database Schema
242
+ ### SNS Signature Verification
182
243
 
183
- The engine creates three tables (prefixed `ses_dashboard_`):
244
+ When `verify_sns_signature = true`, the engine validates the RSA signature on every incoming SNS message before processing it. Both `SignatureVersion` `"1"` (SHA1) and `"2"` (SHA256) are supported.
184
245
 
185
- | Table | Key Columns |
186
- |---|---|
187
- | `ses_dashboard_projects` | `name`, `token` (unique, auto-generated), `description` |
188
- | `ses_dashboard_emails` | `project_id`, `message_id` (unique), `source`, `destination` (JSON), `subject`, `status`, `opens`, `clicks`, `sent_at` |
189
- | `ses_dashboard_email_events` | `email_id`, `event_type`, `event_data` (JSON), `occurred_at` |
246
+ For **raw message delivery** (SNS subscription setting), signature verification is automatically skipped as SNS does not include signature fields in raw payloads — the project token in the URL provides authentication instead.
190
247
 
191
- Email statuses: `sent`, `delivered`, `bounced`, `complained`, `rejected`, `failed`.
248
+ Enable in production:
249
+
250
+ ```ruby
251
+ c.verify_sns_signature = Rails.env.production?
252
+ ```
192
253
 
193
254
  ## Development
194
255
 
@@ -226,13 +287,27 @@ bundle exec rspec spec/models/ses_dashboard/email_spec.rb:15
226
287
  | Service | Purpose | Ports |
227
288
  |---|---|---|
228
289
  | `localstack` | Local AWS (SES + SNS) | 4566 |
229
- | `chrome` | Selenium standalone Chromium | 4444 (WebDriver), 7900 (noVNC -- watch tests live) |
290
+ | `chrome` | Selenium standalone Chromium | 4444 (WebDriver), 7900 (noVNC watch tests live) |
230
291
  | `web` | Runs the test suite | 4001 (Puma) |
231
292
 
232
293
  ### Watching System Tests
233
294
 
234
295
  Open http://localhost:7900 in your browser (no password) to watch Chrome execute system specs in real time via noVNC.
235
296
 
297
+ ## Database Schema
298
+
299
+ The engine creates three tables (prefixed `ses_dashboard_`):
300
+
301
+ | Table | Key Columns |
302
+ |---|---|
303
+ | `ses_dashboard_projects` | `name`, `token` (unique, auto-generated), `description` |
304
+ | `ses_dashboard_emails` | `project_id`, `message_id` (unique), `source`, `destination` (JSON), `subject`, `status`, `opens`, `clicks`, `sent_at` |
305
+ | `ses_dashboard_email_events` | `email_id`, `event_type`, `event_data` (JSON), `occurred_at` |
306
+
307
+ Email statuses: `sent`, `delivered`, `bounced`, `complained`, `rejected`, `failed`.
308
+
309
+ Migrations are compatible with Rails 7.x and 8.x — the migration version is resolved automatically from the host app's Rails version at install time.
310
+
236
311
  ## License
237
312
 
238
313
  MIT
@@ -9,7 +9,11 @@ module SesDashboard
9
9
 
10
10
  def create
11
11
  project = Project.find_by!(token: params[:project_token])
12
- body = request.body.read
12
+ request.body.rewind
13
+ body = request.body.read
14
+ sns = parse_sns_json(body)
15
+
16
+ verify_sns_signature!(sns) if sns && SesDashboard.configuration.verify_sns_signature
13
17
 
14
18
  result = WebhookProcessor.new(body).process
15
19
 
@@ -23,6 +27,9 @@ module SesDashboard
23
27
  head :ok
24
28
  rescue ActiveRecord::RecordNotFound
25
29
  head :not_found
30
+ rescue SnsSignatureVerifier::VerificationError => e
31
+ Rails.logger.warn("[SesDashboard] SNS signature rejected: #{e.message}") if defined?(Rails)
32
+ head :forbidden
26
33
  rescue => e
27
34
  Rails.logger.error("[SesDashboard] Webhook error: #{e.message}") if defined?(Rails)
28
35
  head :unprocessable_entity
@@ -35,5 +42,18 @@ module SesDashboard
35
42
  rescue => e
36
43
  Rails.logger.warn("[SesDashboard] SNS subscription confirm failed: #{e.message}") if defined?(Rails)
37
44
  end
45
+
46
+ def parse_sns_json(body)
47
+ JSON.parse(body)
48
+ rescue JSON::ParserError
49
+ nil
50
+ end
51
+
52
+ def verify_sns_signature!(sns)
53
+ # Skip verification for raw delivery — no SNS envelope means no signature fields.
54
+ return unless sns&.key?("SigningCertURL")
55
+
56
+ SnsSignatureVerifier.new(sns).verify!
57
+ end
38
58
  end
39
59
  end
@@ -1,4 +1,10 @@
1
- class CreateSesDashboardProjects < ActiveRecord::Migration[8.0]
1
+ _migration = begin
2
+ ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"]
3
+ rescue ArgumentError
4
+ ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.0"]
5
+ end
6
+
7
+ class CreateSesDashboardProjects < _migration
2
8
  def change
3
9
  create_table :ses_dashboard_projects do |t|
4
10
  t.string :name, null: false
@@ -1,4 +1,10 @@
1
- class CreateSesDashboardEmails < ActiveRecord::Migration[8.0]
1
+ _migration = begin
2
+ ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"]
3
+ rescue ArgumentError
4
+ ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.0"]
5
+ end
6
+
7
+ class CreateSesDashboardEmails < _migration
2
8
  def change
3
9
  create_table :ses_dashboard_emails do |t|
4
10
  t.references :project, null: false, foreign_key: { to_table: :ses_dashboard_projects }
@@ -1,4 +1,10 @@
1
- class CreateSesDashboardEmailEvents < ActiveRecord::Migration[8.0]
1
+ _migration = begin
2
+ ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"]
3
+ rescue ArgumentError
4
+ ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.0"]
5
+ end
6
+
7
+ class CreateSesDashboardEmailEvents < _migration
2
8
  def change
3
9
  create_table :ses_dashboard_email_events do |t|
4
10
  t.references :email, null: false, foreign_key: { to_table: :ses_dashboard_emails }
@@ -0,0 +1 @@
1
+ require "ses_dashboard"
@@ -42,9 +42,9 @@ module SesDashboard
42
42
  # Options: from:, to:, subject:, body:, configuration_set: (optional)
43
43
  def send_email(from:, to:, subject:, body:, configuration_set: nil)
44
44
  params = {
45
- source: from,
46
- destinations: [to],
47
- message: {
45
+ source: from,
46
+ destination: { to_addresses: [to] },
47
+ message: {
48
48
  subject: { data: subject, charset: "UTF-8" },
49
49
  body: { text: { data: body, charset: "UTF-8" } }
50
50
  }
@@ -13,13 +13,6 @@ module SesDashboard
13
13
  g.helper false
14
14
  end
15
15
 
16
- # Make engine migrations available to the host app via `rails ses_dashboard:install:migrations`
17
- initializer "ses_dashboard.add_migrations" do |app|
18
- unless app.root.to_s == root.to_s
19
- config.paths["db/migrate"].to_a.each { |path| app.config.paths["db/migrate"] << path }
20
- end
21
- end
22
-
23
16
  # Precompile engine assets
24
17
  initializer "ses_dashboard.assets" do |app|
25
18
  if app.config.respond_to?(:assets)
@@ -0,0 +1,92 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module SesDashboard
7
+ # Verifies the authenticity of an SNS HTTP POST using AWS's RSA signature.
8
+ #
9
+ # SNS signs messages with SHA1 (SignatureVersion "1") or SHA256
10
+ # (SignatureVersion "2") using a per-region X.509 certificate hosted at
11
+ # a amazonaws.com URL included in every message.
12
+ #
13
+ # Verification steps:
14
+ # 1. Validate the SigningCertURL is from amazonaws.com (prevents substitution attacks)
15
+ # 2. Fetch and parse the X.509 certificate
16
+ # 3. Reconstruct the canonical string-to-sign
17
+ # 4. Verify the Signature against the cert's public key
18
+ #
19
+ class SnsSignatureVerifier
20
+ # Only trust certs hosted on Amazon's own infrastructure.
21
+ CERT_URL_PATTERN = %r{\Ahttps://sns\.[a-z0-9\-]+\.amazonaws\.com/}.freeze
22
+
23
+ class VerificationError < SesDashboard::Error; end
24
+
25
+ def initialize(sns_message)
26
+ @msg = sns_message
27
+ end
28
+
29
+ # Returns true if valid, raises VerificationError if not.
30
+ def verify!
31
+ validate_cert_url!
32
+ cert = fetch_cert
33
+ digest = signature_version == "2" ? OpenSSL::Digest::SHA256.new : OpenSSL::Digest::SHA1.new
34
+
35
+ unless cert.public_key.verify(digest, decoded_signature, string_to_sign)
36
+ raise VerificationError, "SNS signature verification failed"
37
+ end
38
+
39
+ true
40
+ end
41
+
42
+ private
43
+
44
+ def signature_version
45
+ @msg["SignatureVersion"] || "1"
46
+ end
47
+
48
+ def validate_cert_url!
49
+ url = @msg["SigningCertURL"].to_s
50
+ unless url.match?(CERT_URL_PATTERN)
51
+ raise VerificationError, "Invalid SigningCertURL: #{url.inspect}"
52
+ end
53
+ end
54
+
55
+ def fetch_cert
56
+ url = URI(@msg["SigningCertURL"])
57
+ pem = Net::HTTP.get(url)
58
+ OpenSSL::X509::Certificate.new(pem)
59
+ rescue => e
60
+ raise VerificationError, "Failed to fetch signing certificate: #{e.message}"
61
+ end
62
+
63
+ def decoded_signature
64
+ Base64.decode64(@msg["Signature"].to_s)
65
+ end
66
+
67
+ # AWS canonical string-to-sign — field order is fixed per message type.
68
+ # https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html
69
+ def string_to_sign
70
+ fields = case @msg["Type"]
71
+ when "Notification"
72
+ notification_fields
73
+ when "SubscriptionConfirmation", "UnsubscribeConfirmation"
74
+ subscription_fields
75
+ else
76
+ raise VerificationError, "Unknown SNS message type: #{@msg["Type"].inspect}"
77
+ end
78
+
79
+ fields.map { |key| "#{key}\n#{@msg[key]}\n" }.join
80
+ end
81
+
82
+ def notification_fields
83
+ fields = %w[Message MessageId Subject Timestamp TopicArn Type]
84
+ # Subject is optional — omit if absent (AWS does the same)
85
+ @msg["Subject"] ? fields : fields - ["Subject"]
86
+ end
87
+
88
+ def subscription_fields
89
+ %w[Message MessageId SubscribeURL Timestamp Token TopicArn Type]
90
+ end
91
+ end
92
+ end
@@ -1,3 +1,3 @@
1
1
  module SesDashboard
2
- VERSION = "0.1.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -42,7 +42,9 @@ module SesDashboard
42
42
  when "Notification"
43
43
  process_notification(sns)
44
44
  else
45
- unknown_result
45
+ # SNS raw message delivery — the body is the SES event payload directly,
46
+ # with no SNS envelope. Treat it as a notification message directly.
47
+ process_raw_ses_event(sns)
46
48
  end
47
49
  rescue => e
48
50
  Rails.logger.error("[SesDashboard] WebhookProcessor error: #{e.message}") if defined?(Rails)
@@ -55,6 +57,16 @@ module SesDashboard
55
57
  message = parse_json(sns["Message"])
56
58
  return unknown_result unless message
57
59
 
60
+ process_ses_message(message, sns["Timestamp"])
61
+ end
62
+
63
+ # SNS raw message delivery — body is the SES event directly, no envelope.
64
+ def process_raw_ses_event(message)
65
+ process_ses_message(message, nil)
66
+ end
67
+
68
+ def process_ses_message(message, sns_timestamp)
69
+
58
70
  # SES supports two notification formats:
59
71
  # - Event Publishing (newer): uses "eventType" key
60
72
  # - Feedback Notifications (legacy): uses "notificationType" key
@@ -62,7 +74,7 @@ module SesDashboard
62
74
  event_type = normalize_event_type(raw_event_type)
63
75
 
64
76
  mail = message["mail"] || {}
65
- timestamp = parse_time(mail["timestamp"] || sns["Timestamp"])
77
+ timestamp = parse_time(mail["timestamp"] || sns_timestamp)
66
78
 
67
79
  Result.new(
68
80
  action: :process_event,
data/lib/ses_dashboard.rb CHANGED
@@ -1,12 +1,3 @@
1
- require_relative "ses_dashboard/version"
2
- require_relative "ses_dashboard/client"
3
- require_relative "ses_dashboard/webhook_processor"
4
- require_relative "ses_dashboard/stats_aggregator"
5
- require_relative "ses_dashboard/paginatable"
6
- require_relative "ses_dashboard/auth/base"
7
- require_relative "ses_dashboard/auth/devise_adapter"
8
- require_relative "ses_dashboard/auth/cloudflare_adapter"
9
-
10
1
  module SesDashboard
11
2
  class Error < StandardError; end
12
3
 
@@ -61,6 +52,15 @@ module SesDashboard
61
52
  @cloudflare_aud = nil
62
53
  end
63
54
  end
64
-
65
- autoload :Engine, "ses_dashboard/engine"
66
55
  end
56
+
57
+ require_relative "ses_dashboard/version"
58
+ require_relative "ses_dashboard/client"
59
+ require_relative "ses_dashboard/webhook_processor"
60
+ require_relative "ses_dashboard/sns_signature_verifier"
61
+ require_relative "ses_dashboard/stats_aggregator"
62
+ require_relative "ses_dashboard/paginatable"
63
+ require_relative "ses_dashboard/auth/base"
64
+ require_relative "ses_dashboard/auth/devise_adapter"
65
+ require_relative "ses_dashboard/auth/cloudflare_adapter"
66
+ require "ses_dashboard/engine"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ses-dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - antodoms
@@ -332,6 +332,7 @@ files:
332
332
  - db/migrate/20240101000002_create_ses_dashboard_emails.rb
333
333
  - db/migrate/20240101000003_create_ses_dashboard_email_events.rb
334
334
  - docker-compose.yml
335
+ - lib/ses/dashboard.rb
335
336
  - lib/ses_dashboard.rb
336
337
  - lib/ses_dashboard/auth/base.rb
337
338
  - lib/ses_dashboard/auth/cloudflare_adapter.rb
@@ -339,6 +340,7 @@ files:
339
340
  - lib/ses_dashboard/client.rb
340
341
  - lib/ses_dashboard/engine.rb
341
342
  - lib/ses_dashboard/paginatable.rb
343
+ - lib/ses_dashboard/sns_signature_verifier.rb
342
344
  - lib/ses_dashboard/stats_aggregator.rb
343
345
  - lib/ses_dashboard/version.rb
344
346
  - lib/ses_dashboard/webhook_processor.rb