rails_webhook_outbox 0.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1
  3. data/Rakefile +6 -1
  4. data/UPGRADING.md +92 -0
  5. data/app/assets/stylesheets/rails_webhook_outbox/_01_base.css +53 -0
  6. data/app/assets/stylesheets/rails_webhook_outbox/_02_layout.css +88 -0
  7. data/app/assets/stylesheets/rails_webhook_outbox/_03_table.css +26 -0
  8. data/app/assets/stylesheets/rails_webhook_outbox/_04_badges.css +19 -0
  9. data/app/assets/stylesheets/rails_webhook_outbox/_05_forms.css +42 -0
  10. data/app/assets/stylesheets/rails_webhook_outbox/_06_filters.css +13 -0
  11. data/app/assets/stylesheets/rails_webhook_outbox/_07_timeline.css +31 -0
  12. data/app/controllers/rails_webhook_outbox/base_controller.rb +15 -0
  13. data/app/controllers/rails_webhook_outbox/deliveries_controller.rb +46 -0
  14. data/app/controllers/rails_webhook_outbox/events_controller.rb +13 -0
  15. data/app/controllers/rails_webhook_outbox/overview_controller.rb +7 -0
  16. data/app/controllers/rails_webhook_outbox/subscriptions_controller.rb +62 -0
  17. data/app/helpers/rails_webhook_outbox/application_helper.rb +29 -0
  18. data/app/javascript/rails_webhook_outbox/application.js +2 -0
  19. data/app/javascript/rails_webhook_outbox/controllers/application.js +4 -0
  20. data/app/javascript/rails_webhook_outbox/controllers/index.js +4 -0
  21. data/app/javascript/rails_webhook_outbox/controllers/secret_controller.js +24 -0
  22. data/app/jobs/rails_webhook_outbox/delivery_job.rb +15 -0
  23. data/app/models/rails_webhook_outbox/delivery.rb +28 -0
  24. data/app/models/rails_webhook_outbox/delivery_attempt.rb +13 -0
  25. data/app/models/rails_webhook_outbox/overview.rb +49 -0
  26. data/app/models/rails_webhook_outbox/subscription.rb +6 -1
  27. data/app/views/layouts/rails_webhook_outbox/application.html.erb +22 -0
  28. data/app/views/rails_webhook_outbox/deliveries/_detail.html.erb +47 -0
  29. data/app/views/rails_webhook_outbox/deliveries/_row.html.erb +7 -0
  30. data/app/views/rails_webhook_outbox/deliveries/_timeline.html.erb +41 -0
  31. data/app/views/rails_webhook_outbox/deliveries/index.html.erb +62 -0
  32. data/app/views/rails_webhook_outbox/deliveries/retry.turbo_stream.erb +2 -0
  33. data/app/views/rails_webhook_outbox/deliveries/show.html.erb +5 -0
  34. data/app/views/rails_webhook_outbox/events/index.html.erb +22 -0
  35. data/app/views/rails_webhook_outbox/overview/_recent_failures.html.erb +29 -0
  36. data/app/views/rails_webhook_outbox/overview/_stats.html.erb +19 -0
  37. data/app/views/rails_webhook_outbox/overview/show.html.erb +8 -0
  38. data/app/views/rails_webhook_outbox/subscriptions/_form.html.erb +49 -0
  39. data/app/views/rails_webhook_outbox/subscriptions/edit.html.erb +7 -0
  40. data/app/views/rails_webhook_outbox/subscriptions/index.html.erb +37 -0
  41. data/app/views/rails_webhook_outbox/subscriptions/new.html.erb +7 -0
  42. data/app/views/rails_webhook_outbox/subscriptions/show.html.erb +49 -0
  43. data/config/importmap.rb +6 -0
  44. data/config/routes.rb +17 -0
  45. data/db/migrate/20260624000001_create_webhook_outbox_subscriptions.rb +1 -1
  46. data/db/migrate/20260624000002_create_webhook_outbox_deliveries.rb +4 -4
  47. data/db/migrate/20260629000002_add_idempotency_key_to_webhook_outbox_deliveries.rb +2 -2
  48. data/db/migrate/20260701000001_add_secret_rotation_to_webhook_outbox_subscriptions.rb +2 -2
  49. data/db/migrate/20260701000002_add_consecutive_failures_to_webhook_outbox_subscriptions.rb +1 -1
  50. data/db/migrate/20261027000001_create_webhook_outbox_delivery_attempts.rb +16 -0
  51. data/lib/generators/rails_webhook_outbox/install/install_generator.rb +1 -19
  52. data/lib/rails_webhook_outbox/configuration.rb +3 -1
  53. data/lib/rails_webhook_outbox/engine.rb +16 -0
  54. data/lib/rails_webhook_outbox/version.rb +1 -1
  55. metadata +67 -3
  56. data/lib/generators/rails_webhook_outbox/install/templates/create_webhook_outbox_deliveries.rb.tt +0 -21
  57. data/lib/generators/rails_webhook_outbox/install/templates/create_webhook_outbox_subscriptions.rb.tt +0 -16
@@ -0,0 +1,49 @@
1
+ <%= link_to "← All Subscriptions", subscriptions_path, class: "rwo-back" %>
2
+
3
+ <div class="rwo-header rwo-header-row">
4
+ <h1><%= @subscription.url %></h1>
5
+ <%= link_to "Edit", edit_subscription_path(@subscription), class: "rwo-btn" %>
6
+ </div>
7
+
8
+ <% if params[:rotation_error] %>
9
+ <p class="rwo-alert">Secret rotation already in progress — the previous secret is still within its grace period.</p>
10
+ <% end %>
11
+
12
+ <div class="rwo-detail">
13
+ <div class="rwo-detail-grid">
14
+ <div class="rwo-detail-item">
15
+ <div class="rwo-detail-label">Status</div>
16
+ <div class="rwo-detail-value"><%= active_badge(@subscription.active?) %></div>
17
+ </div>
18
+ <div class="rwo-detail-item">
19
+ <div class="rwo-detail-label">Events</div>
20
+ <div class="rwo-detail-value"><%= @subscription.events.join(", ") %></div>
21
+ </div>
22
+ <div class="rwo-detail-item">
23
+ <div class="rwo-detail-label">Consecutive Failures</div>
24
+ <div class="rwo-detail-value"><%= @subscription.consecutive_failures %></div>
25
+ </div>
26
+ <div class="rwo-detail-item">
27
+ <div class="rwo-detail-label">Description</div>
28
+ <div class="rwo-detail-value"><%= @subscription.description.presence || "—" %></div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <h2 class="rwo-section-title">Signing Secret</h2>
34
+
35
+ <div class="rwo-secret" data-controller="secret" data-secret-value-value="<%= @subscription.secret %>">
36
+ <span class="rwo-secret-display rwo-monospace" data-secret-target="display"><%= masked_secret(@subscription.secret) %></span>
37
+ <button type="button" class="rwo-btn-small" data-action="click->secret#reveal" data-secret-target="revealButton">Reveal</button>
38
+ <button type="button" class="rwo-btn-small" data-action="click->secret#copy" data-secret-target="copyButton">Copy</button>
39
+ <span class="rwo-muted" hidden data-secret-target="copiedHint">Copied!</span>
40
+ </div>
41
+
42
+ <% if @subscription.previous_secret_active? %>
43
+ <p class="rwo-muted">Previous secret valid until <%= @subscription.previous_secret_expires_at.strftime("%Y-%m-%d %H:%M:%S") %>.</p>
44
+ <% end %>
45
+
46
+ <%= button_to "Rotate Secret", rotate_secret_subscription_path(@subscription),
47
+ method: :patch,
48
+ class: "rwo-btn-danger",
49
+ form: { data: { turbo_confirm: "Rotate the signing secret? Subscribers must be updated within the grace period." } } %>
@@ -0,0 +1,6 @@
1
+ pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
2
+ pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
3
+ pin "rails_webhook_outbox", to: "rails_webhook_outbox/application.js"
4
+ pin "rails_webhook_outbox/controllers", to: "rails_webhook_outbox/controllers/index.js"
5
+ pin "rails_webhook_outbox/controllers/application", to: "rails_webhook_outbox/controllers/application.js"
6
+ pin "rails_webhook_outbox/controllers/secret_controller", to: "rails_webhook_outbox/controllers/secret_controller.js"
data/config/routes.rb CHANGED
@@ -1,2 +1,19 @@
1
1
  RailsWebhookOutbox::Engine.routes.draw do
2
+ root to: "overview#show"
3
+
4
+ # Explicit :only keeps new/edit routes on Rails 7.2 in api_only host apps,
5
+ # where resources would otherwise omit them.
6
+ resources :subscriptions, only: [:index, :show, :new, :create, :edit, :update, :destroy] do
7
+ member do
8
+ patch :rotate_secret
9
+ end
10
+ end
11
+
12
+ resources :deliveries, only: [:index, :show] do
13
+ member do
14
+ post :retry
15
+ end
16
+ end
17
+
18
+ resources :events, only: [:index]
2
19
  end
@@ -1,6 +1,6 @@
1
1
  class CreateWebhookOutboxSubscriptions < ActiveRecord::Migration[7.2]
2
2
  def change
3
- create_table :webhook_outbox_subscriptions do |t|
3
+ create_table :webhook_outbox_subscriptions, if_not_exists: true do |t|
4
4
  t.string :url, null: false
5
5
  t.string :secret, null: false
6
6
  t.json :events, null: false, default: []
@@ -1,6 +1,6 @@
1
1
  class CreateWebhookOutboxDeliveries < ActiveRecord::Migration[7.2]
2
2
  def change
3
- create_table :webhook_outbox_deliveries do |t|
3
+ create_table :webhook_outbox_deliveries, if_not_exists: true do |t|
4
4
  t.references :subscription, null: false, foreign_key: { to_table: :webhook_outbox_subscriptions }
5
5
  t.string :event, null: false
6
6
  t.json :payload, null: false
@@ -13,8 +13,8 @@ class CreateWebhookOutboxDeliveries < ActiveRecord::Migration[7.2]
13
13
  t.timestamps
14
14
  end
15
15
 
16
- add_index :webhook_outbox_deliveries, :status
17
- add_index :webhook_outbox_deliveries, :event
18
- add_index :webhook_outbox_deliveries, [:subscription_id, :created_at]
16
+ add_index :webhook_outbox_deliveries, :status, if_not_exists: true
17
+ add_index :webhook_outbox_deliveries, :event, if_not_exists: true
18
+ add_index :webhook_outbox_deliveries, [:subscription_id, :created_at], if_not_exists: true
19
19
  end
20
20
  end
@@ -1,6 +1,6 @@
1
1
  class AddIdempotencyKeyToWebhookOutboxDeliveries < ActiveRecord::Migration[7.2]
2
2
  def change
3
- add_column :webhook_outbox_deliveries, :idempotency_key, :string
4
- add_index :webhook_outbox_deliveries, :idempotency_key, unique: true
3
+ add_column :webhook_outbox_deliveries, :idempotency_key, :string, if_not_exists: true
4
+ add_index :webhook_outbox_deliveries, :idempotency_key, unique: true, if_not_exists: true
5
5
  end
6
6
  end
@@ -1,6 +1,6 @@
1
1
  class AddSecretRotationToWebhookOutboxSubscriptions < ActiveRecord::Migration[7.2]
2
2
  def change
3
- add_column :webhook_outbox_subscriptions, :previous_secret, :string
4
- add_column :webhook_outbox_subscriptions, :previous_secret_expires_at, :datetime
3
+ add_column :webhook_outbox_subscriptions, :previous_secret, :string, if_not_exists: true
4
+ add_column :webhook_outbox_subscriptions, :previous_secret_expires_at, :datetime, if_not_exists: true
5
5
  end
6
6
  end
@@ -1,5 +1,5 @@
1
1
  class AddConsecutiveFailuresToWebhookOutboxSubscriptions < ActiveRecord::Migration[7.2]
2
2
  def change
3
- add_column :webhook_outbox_subscriptions, :consecutive_failures, :integer, null: false, default: 0
3
+ add_column :webhook_outbox_subscriptions, :consecutive_failures, :integer, null: false, default: 0, if_not_exists: true
4
4
  end
5
5
  end
@@ -0,0 +1,16 @@
1
+ class CreateWebhookOutboxDeliveryAttempts < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :webhook_outbox_delivery_attempts, if_not_exists: true do |t|
4
+ t.references :delivery, null: false, index: false,
5
+ foreign_key: { to_table: :webhook_outbox_deliveries, on_delete: :cascade }
6
+ t.integer :attempt_number, null: false
7
+ t.integer :outcome, null: false
8
+ t.integer :response_code
9
+ t.text :response_body
10
+ t.integer :duration_ms
11
+ t.datetime :created_at, null: false
12
+ end
13
+
14
+ add_index :webhook_outbox_delivery_attempts, [:delivery_id, :attempt_number], if_not_exists: true
15
+ end
16
+ end
@@ -1,29 +1,11 @@
1
1
  require "rails/generators"
2
- require "rails/generators/active_record"
3
2
 
4
3
  module RailsWebhookOutbox
5
4
  module Generators
6
5
  class InstallGenerator < Rails::Generators::Base
7
- include Rails::Generators::Migration
8
-
9
6
  source_root File.expand_path("templates", __dir__)
10
7
 
11
- desc "Creates a RailsWebhookOutbox initializer and copies migrations to your application."
12
-
13
- def self.next_migration_number(dirname)
14
- ActiveRecord::Generators::Base.next_migration_number(dirname)
15
- end
16
-
17
- def copy_migrations
18
- migration_template(
19
- "create_webhook_outbox_subscriptions.rb.tt",
20
- "db/migrate/create_webhook_outbox_subscriptions.rb"
21
- )
22
- migration_template(
23
- "create_webhook_outbox_deliveries.rb.tt",
24
- "db/migrate/create_webhook_outbox_deliveries.rb"
25
- )
26
- end
8
+ desc "Creates a RailsWebhookOutbox initializer. The engine supplies its own migrations; run `rails db:migrate` afterwards."
27
9
 
28
10
  def create_initializer
29
11
  template "initializer.rb", "config/initializers/rails_webhook_outbox.rb"
@@ -6,7 +6,8 @@ module RailsWebhookOutbox
6
6
  attr_accessor :events, :signing_algorithm, :signing_header,
7
7
  :max_retries, :retry_backoff, :request_timeout,
8
8
  :delivery_job_queue, :max_payload_size, :test_mode,
9
- :secret_rotation_grace_period, :circuit_breaker_threshold
9
+ :secret_rotation_grace_period, :circuit_breaker_threshold,
10
+ :dashboard_enabled
10
11
 
11
12
  def initialize
12
13
  @events = []
@@ -20,6 +21,7 @@ module RailsWebhookOutbox
20
21
  @test_mode = false
21
22
  @secret_rotation_grace_period = 24.hours
22
23
  @circuit_breaker_threshold = 10
24
+ @dashboard_enabled = Rails.env.development?
23
25
  end
24
26
 
25
27
  def signing_algorithm=(value)
@@ -1,3 +1,6 @@
1
+ require "turbo-rails"
2
+ require "importmap-rails"
3
+
1
4
  module RailsWebhookOutbox
2
5
  class Engine < ::Rails::Engine
3
6
  isolate_namespace RailsWebhookOutbox
@@ -10,5 +13,18 @@ module RailsWebhookOutbox
10
13
  end
11
14
  end
12
15
  end
16
+
17
+ initializer "rails_webhook_outbox.assets" do |app|
18
+ if app.config.respond_to?(:assets)
19
+ app.config.assets.paths << RailsWebhookOutbox::Engine.root.join("app/javascript")
20
+ end
21
+ end
22
+
23
+ initializer "rails_webhook_outbox.importmap", before: "importmap" do |app|
24
+ if app.config.respond_to?(:importmap)
25
+ app.config.importmap.paths << RailsWebhookOutbox::Engine.root.join("config/importmap.rb")
26
+ app.config.importmap.cache_sweepers << RailsWebhookOutbox::Engine.root.join("app/javascript")
27
+ end
28
+ end
13
29
  end
14
30
  end
@@ -1,3 +1,3 @@
1
1
  module RailsWebhookOutbox
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_webhook_outbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -23,6 +23,34 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '7.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: importmap-rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: turbo-rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
26
54
  description: A Rails engine for sending outgoing webhooks with HMAC signing, ActiveJob-based
27
55
  retry, and delivery logging.
28
56
  email:
@@ -34,22 +62,58 @@ files:
34
62
  - MIT-LICENSE
35
63
  - README.md
36
64
  - Rakefile
65
+ - UPGRADING.md
66
+ - app/assets/stylesheets/rails_webhook_outbox/_01_base.css
67
+ - app/assets/stylesheets/rails_webhook_outbox/_02_layout.css
68
+ - app/assets/stylesheets/rails_webhook_outbox/_03_table.css
69
+ - app/assets/stylesheets/rails_webhook_outbox/_04_badges.css
70
+ - app/assets/stylesheets/rails_webhook_outbox/_05_forms.css
71
+ - app/assets/stylesheets/rails_webhook_outbox/_06_filters.css
72
+ - app/assets/stylesheets/rails_webhook_outbox/_07_timeline.css
37
73
  - app/controllers/rails_webhook_outbox/application_controller.rb
74
+ - app/controllers/rails_webhook_outbox/base_controller.rb
75
+ - app/controllers/rails_webhook_outbox/deliveries_controller.rb
76
+ - app/controllers/rails_webhook_outbox/events_controller.rb
77
+ - app/controllers/rails_webhook_outbox/overview_controller.rb
78
+ - app/controllers/rails_webhook_outbox/subscriptions_controller.rb
79
+ - app/helpers/rails_webhook_outbox/application_helper.rb
80
+ - app/javascript/rails_webhook_outbox/application.js
81
+ - app/javascript/rails_webhook_outbox/controllers/application.js
82
+ - app/javascript/rails_webhook_outbox/controllers/index.js
83
+ - app/javascript/rails_webhook_outbox/controllers/secret_controller.js
38
84
  - app/jobs/rails_webhook_outbox/application_job.rb
39
85
  - app/jobs/rails_webhook_outbox/delivery_job.rb
40
86
  - app/mailers/rails_webhook_outbox/application_mailer.rb
41
87
  - app/models/rails_webhook_outbox/application_record.rb
42
88
  - app/models/rails_webhook_outbox/delivery.rb
89
+ - app/models/rails_webhook_outbox/delivery_attempt.rb
90
+ - app/models/rails_webhook_outbox/overview.rb
43
91
  - app/models/rails_webhook_outbox/subscription.rb
92
+ - app/views/layouts/rails_webhook_outbox/application.html.erb
93
+ - app/views/rails_webhook_outbox/deliveries/_detail.html.erb
94
+ - app/views/rails_webhook_outbox/deliveries/_row.html.erb
95
+ - app/views/rails_webhook_outbox/deliveries/_timeline.html.erb
96
+ - app/views/rails_webhook_outbox/deliveries/index.html.erb
97
+ - app/views/rails_webhook_outbox/deliveries/retry.turbo_stream.erb
98
+ - app/views/rails_webhook_outbox/deliveries/show.html.erb
99
+ - app/views/rails_webhook_outbox/events/index.html.erb
100
+ - app/views/rails_webhook_outbox/overview/_recent_failures.html.erb
101
+ - app/views/rails_webhook_outbox/overview/_stats.html.erb
102
+ - app/views/rails_webhook_outbox/overview/show.html.erb
103
+ - app/views/rails_webhook_outbox/subscriptions/_form.html.erb
104
+ - app/views/rails_webhook_outbox/subscriptions/edit.html.erb
105
+ - app/views/rails_webhook_outbox/subscriptions/index.html.erb
106
+ - app/views/rails_webhook_outbox/subscriptions/new.html.erb
107
+ - app/views/rails_webhook_outbox/subscriptions/show.html.erb
108
+ - config/importmap.rb
44
109
  - config/routes.rb
45
110
  - db/migrate/20260624000001_create_webhook_outbox_subscriptions.rb
46
111
  - db/migrate/20260624000002_create_webhook_outbox_deliveries.rb
47
112
  - db/migrate/20260629000002_add_idempotency_key_to_webhook_outbox_deliveries.rb
48
113
  - db/migrate/20260701000001_add_secret_rotation_to_webhook_outbox_subscriptions.rb
49
114
  - db/migrate/20260701000002_add_consecutive_failures_to_webhook_outbox_subscriptions.rb
115
+ - db/migrate/20261027000001_create_webhook_outbox_delivery_attempts.rb
50
116
  - lib/generators/rails_webhook_outbox/install/install_generator.rb
51
- - lib/generators/rails_webhook_outbox/install/templates/create_webhook_outbox_deliveries.rb.tt
52
- - lib/generators/rails_webhook_outbox/install/templates/create_webhook_outbox_subscriptions.rb.tt
53
117
  - lib/generators/rails_webhook_outbox/install/templates/initializer.rb
54
118
  - lib/rails_webhook_outbox.rb
55
119
  - lib/rails_webhook_outbox/configuration.rb
@@ -1,21 +0,0 @@
1
- class CreateWebhookOutboxDeliveries < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
- def change
3
- create_table :webhook_outbox_deliveries do |t|
4
- t.references :subscription, null: false, foreign_key: { to_table: :webhook_outbox_subscriptions }
5
- t.string :event, null: false
6
- t.json :payload, null: false
7
- t.integer :status, null: false, default: 0
8
- t.integer :response_code
9
- t.text :response_body
10
- t.integer :attempts, null: false, default: 0
11
- t.string :idempotency_key
12
- t.datetime :delivered_at
13
- t.datetime :next_retry_at
14
- t.timestamps
15
- end
16
-
17
- add_index :webhook_outbox_deliveries, :status
18
- add_index :webhook_outbox_deliveries, :event
19
- add_index :webhook_outbox_deliveries, [:subscription_id, :created_at]
20
- end
21
- end
@@ -1,16 +0,0 @@
1
- class CreateWebhookOutboxSubscriptions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
- def change
3
- create_table :webhook_outbox_subscriptions do |t|
4
- t.string :url, null: false
5
- t.string :secret, null: false
6
- t.string :previous_secret
7
- t.datetime :previous_secret_expires_at
8
- t.json :events, null: false, default: []
9
- t.boolean :active, null: false, default: true
10
- t.integer :consecutive_failures, null: false, default: 0
11
- t.string :description
12
- t.json :metadata, default: {}
13
- t.timestamps
14
- end
15
- end
16
- end