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: "ping"}, force: true) ⇒ Object
Deliver a synthetic ping event (name from config.ping_event_name, default "ping") 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
56 57 58 |
# File 'app/models/angarium/endpoint.rb', line 56 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.
68 69 70 71 72 73 74 75 |
# File 'app/models/angarium/endpoint.rb', line 68 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)
116 117 118 119 |
# File 'app/models/angarium/endpoint.rb', line 116 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).
128 129 130 131 132 133 134 135 136 137 |
# File 'app/models/angarium/endpoint.rb', line 128 def enable! return false unless transition_status!(:enabled, consecutive_failures: 0) # Resume deliveries parked while this endpoint was paused (pending with no # scheduled attempt). This predicate can also match a delivery that already # has an in-flight job (a forced ping, a just-redelivered row), but a stray # re-enqueue is harmless: the atomic pending->delivering claim in # Delivery#deliver! guarantees only one worker ever sends a given delivery. 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!.
122 123 124 |
# File 'app/models/angarium/endpoint.rb', line 122 def pause! transition_status!(:paused) end |
#ping!(payload = {message: "ping"}, force: true) ⇒ Object
Deliver a synthetic ping event (name from config.ping_event_name, default
"ping") 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.
154 155 156 157 |
# File 'app/models/angarium/endpoint.rb', line 154 def ping!(payload = {message: "ping"}, force: true) event = Angarium::Event.create!(name: Angarium.config.ping_event_name, payload: payload) deliveries.create!(event: event, forced: force) end |
#record_delivery_failure! ⇒ Object
98 99 100 101 102 103 104 105 106 107 108 |
# File 'app/models/angarium/endpoint.rb', line 98 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
90 91 92 93 94 95 96 |
# File 'app/models/angarium/endpoint.rb', line 90 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.
81 82 83 84 85 86 87 88 |
# File 'app/models/angarium/endpoint.rb', line 81 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
60 61 62 |
# File 'app/models/angarium/endpoint.rb', line 60 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.
144 145 146 |
# File 'app/models/angarium/endpoint.rb', line 144 def verify! transition_status!(:enabled, from: :unverified) { Angarium.notify(:on_endpoint_verified, self) } end |