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
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
50 51 52 |
# File 'app/models/angarium/endpoint.rb', line 50 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.
62 63 64 65 66 67 68 69 |
# File 'app/models/angarium/endpoint.rb', line 62 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)
110 111 112 113 |
# File 'app/models/angarium/endpoint.rb', line 110 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).
122 123 124 125 126 127 128 129 130 131 |
# File 'app/models/angarium/endpoint.rb', line 122 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!.
116 117 118 |
# File 'app/models/angarium/endpoint.rb', line 116 def pause! transition_status!(:paused) end |
#ping!(payload = {message: "ping"}, force: true) ⇒ Object
Deliver a synthetic 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.
148 149 150 151 |
# File 'app/models/angarium/endpoint.rb', line 148 def ping!(payload = {message: "ping"}, force: true) event = Angarium::Event.create!(name: "ping", payload: payload) deliveries.create!(event: event, forced: force) end |
#record_delivery_failure! ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 |
# File 'app/models/angarium/endpoint.rb', line 92 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
84 85 86 87 88 89 90 |
# File 'app/models/angarium/endpoint.rb', line 84 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.
75 76 77 78 79 80 81 82 |
# File 'app/models/angarium/endpoint.rb', line 75 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
54 55 56 |
# File 'app/models/angarium/endpoint.rb', line 54 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.
138 139 140 |
# File 'app/models/angarium/endpoint.rb', line 138 def verify! transition_status!(:enabled, from: :unverified) { Angarium.notify(:on_endpoint_verified, self) } end |