Class: Angarium::Endpoint
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Angarium::Endpoint
- Defined in:
- app/models/angarium/endpoint.rb
Constant Summary collapse
- RESERVED_HEADERS =
Headers a user-supplied custom header may never set: the Standard Webhooks signature headers (which must not be spoofable) and transport/hop-by-hop headers whose override invites request smuggling or receiver confusion.
%w[ webhook-id webhook-timestamp webhook-signature host content-length content-type transfer-encoding connection ].freeze
Class Method Summary collapse
Instance Method Summary collapse
-
#active_signing_secrets ⇒ Object
Secrets a receiver may currently hold: always the active secret, plus the previous one while it's still within the rotation grace window.
-
#deactivate!(reason:) ⇒ Object
Move to a non-delivering state (no further deliveries) and fire the on_endpoint_deactivated callback once.
-
#enable! ⇒ Object
(Re-)enable deliveries and clear the failure counter.
-
#pause! ⇒ Object
Pause deliveries manually.
-
#ping!(payload = {message: "Angarium ping"}, force: true) ⇒ Object
Deliver a synthetic
angarium.pingevent to this endpoint, bypassing subscription matching (a ping is always sent). - #record_delivery_failure! ⇒ Object
- #record_delivery_success! ⇒ Object
-
#rotate_secret! ⇒ Object
Rotate the signing secret and persist.
- #subscribed_to?(event_name) ⇒ Boolean
-
#verify! ⇒ Object
Promote an
unverifiedendpoint toenabledonce it has proven it can receive webhooks.
Class Method Details
.generate_signing_secret ⇒ Object
48 49 50 |
# File 'app/models/angarium/endpoint.rb', line 48 def self.generate_signing_secret "whsec_#{Base64.strict_encode64(SecureRandom.bytes(32))}" end |
Instance Method Details
#active_signing_secrets ⇒ Object
Secrets a receiver may currently hold: always the active secret, plus the previous one while it's still within the rotation grace window. Deliveries sign with every returned secret so receivers can roll over with zero downtime.
60 61 62 63 64 65 66 67 |
# File 'app/models/angarium/endpoint.rb', line 60 def active_signing_secrets secrets = [signing_secret] if previous_signing_secret.present? && secret_rotated_at.present? && secret_rotated_at > Angarium.config.signing_secret_grace_period.ago secrets << previous_signing_secret end secrets end |
#deactivate!(reason:) ⇒ Object
Move to a non-delivering state (no further deliveries) and fire the
on_endpoint_deactivated callback once. reason maps to the target status:
:consecutive_failures -> disabled (auto-disable, only from `enabled`)
:gone -> gone (receiver returned HTTP 410; overrides
any non-gone status, so a racing
auto-disable can't clobber a 410)
108 109 110 111 |
# File 'app/models/angarium/endpoint.rb', line 108 def deactivate!(reason:) target, from = (reason == :gone) ? [:gone, nil] : [:disabled, :enabled] transition_status!(target, from: from) { Angarium.notify(:on_endpoint_deactivated, self, reason) } end |
#enable! ⇒ Object
(Re-)enable deliveries and clear the failure counter. Works from any state,
including gone (an explicit operator override of a receiver's 410).
120 121 122 123 124 125 126 127 |
# File 'app/models/angarium/endpoint.rb', line 120 def enable! return false unless transition_status!(:enabled, consecutive_failures: 0) # Resume deliveries parked while this endpoint was paused (pending with no # scheduled attempt). Dispatch creates none while paused, so this only # re-enqueues the held ones. deliveries.where(state: "pending", next_attempt_at: nil).find_each { |d| DeliverJob.perform_later(d.id) } true end |
#pause! ⇒ Object
Pause deliveries manually. Resumable via #enable!.
114 115 116 |
# File 'app/models/angarium/endpoint.rb', line 114 def pause! transition_status!(:paused) end |
#ping!(payload = {message: "Angarium ping"}, force: true) ⇒ Object
Deliver a synthetic angarium.ping event to this endpoint, bypassing
subscription matching (a ping is always sent). By default a ping also
ignores the endpoint's status, so you can test an endpoint that is paused,
disabled, or not yet enabled; pass force: false to respect the status
guard instead. Returns the created Angarium::Delivery, whose
after_create_commit enqueues the DeliverJob; reload it to inspect the outcome.
144 145 146 147 |
# File 'app/models/angarium/endpoint.rb', line 144 def ping!(payload = {message: "Angarium ping"}, force: true) event = Angarium::Event.create!(name: "angarium.ping", payload: payload) deliveries.create!(event: event) { |d| d.force_send = force } end |
#record_delivery_failure! ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 |
# File 'app/models/angarium/endpoint.rb', line 90 def record_delivery_failure! # Atomic increment. Without it, concurrent deliveries to the same endpoint # each read a stale counter and lose increments (last write wins), so # auto-disable would undercount. Reload to read the true post-increment # count before deciding whether to disable. self.class.update_counters(id, consecutive_failures: 1) reload threshold = Angarium.config.auto_disable_endpoint_after deactivate!(reason: :consecutive_failures) if threshold && consecutive_failures >= threshold end |
#record_delivery_success! ⇒ Object
82 83 84 85 86 87 88 |
# File 'app/models/angarium/endpoint.rb', line 82 def record_delivery_success! # Reset from the database, not this (possibly stale) in-memory copy: a # concurrent failed delivery may have raised the counter since we loaded. # The WHERE skips a write when it is already zero. self.class.where(id: id).where.not(consecutive_failures: 0).update_all(consecutive_failures: 0) self.consecutive_failures = 0 end |
#rotate_secret! ⇒ Object
Rotate the signing secret and persist. Returns the new plaintext secret.
The previous secret stays valid for config.signing_secret_grace_period
(deliveries are signed with both during that window), so receivers can
roll over to the new secret with zero downtime.
73 74 75 76 77 78 79 80 |
# File 'app/models/angarium/endpoint.rb', line 73 def rotate_secret! update!( previous_signing_secret: signing_secret, signing_secret: self.class.generate_signing_secret, secret_rotated_at: Time.current ) signing_secret end |
#subscribed_to?(event_name) ⇒ Boolean
52 53 54 |
# File 'app/models/angarium/endpoint.rb', line 52 def subscribed_to?(event_name) Array(subscribed_events).any? { |pattern| EventMatcher.match?(pattern, event_name) } end |
#verify! ⇒ Object
Promote an unverified endpoint to enabled once it has proven it can
receive webhooks. A no-op on any other status: a disabled/gone endpoint
is revived with #enable!, not verified. Called automatically when a delivery
to an unverified endpoint succeeds (a forced #ping!), or manually. Fires
on_endpoint_verified exactly once, even under concurrent verification.
134 135 136 |
# File 'app/models/angarium/endpoint.rb', line 134 def verify! transition_status!(:enabled, from: :unverified) { Angarium.notify(:on_endpoint_verified, self) } end |