Class: Inquirex::Actions::Webhook

Inherits:
Base
  • Object
show all
Defined in:
lib/inquirex/actions/webhook.rb

Overview

Declarative webhook effect: POSTs the completed answers as a JSON envelope (=> {...}) to a statically-declared URL.

Security posture — the URL must be auditable and its host covered by the definition's allowed_domains declaration:

  • the URL is a literal string; {field} templates are rejected so the destination host can never depend on user input
  • https only; plain http is permitted solely for localhost development
  • userinfo (https://user@host/) is rejected
  • redirects are not followed (Net::HTTP does not follow them)
  • the host check runs in Definition#validate!, which every definition passes through — including JSON-rehydrated ones, so a tampered URL fails at load time, before anything executes

A non-2xx response raises Errors::ActionError, which the Runner records as a :failed result without blocking other actions.

Constant Summary collapse

DEFAULT_TIMEOUT =

Default open/read timeout in seconds for the webhook POST.

10
LOCAL_HOSTS =

Hosts for which plain http is tolerated (local development only).

%w[localhost 127.0.0.1 ::1 [::1]].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#serializable?

Constructor Details

#initialize(url:, headers: {}, timeout: DEFAULT_TIMEOUT) ⇒ Webhook

Returns a new instance of Webhook.

Parameters:

  • url (String)

    literal https URL (no {field} templates)

  • headers (Hash) (defaults to: {})

    extra request headers (literal values)

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    open/read timeout in seconds



35
36
37
38
39
40
41
42
# File 'lib/inquirex/actions/webhook.rb', line 35

def initialize(url:, headers: {}, timeout: DEFAULT_TIMEOUT)
  super()
  @url = url.to_s
  @headers = headers.transform_keys(&:to_s).transform_values(&:to_s).freeze
  @timeout = timeout.to_i
  @uri = parse_and_check!(@url)
  freeze
end

Instance Attribute Details

#headersObject (readonly)

Returns the value of attribute headers.



30
31
32
# File 'lib/inquirex/actions/webhook.rb', line 30

def headers
  @headers
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



30
31
32
# File 'lib/inquirex/actions/webhook.rb', line 30

def timeout
  @timeout
end

#urlObject (readonly)

Returns the value of attribute url.



30
31
32
# File 'lib/inquirex/actions/webhook.rb', line 30

def url
  @url
end

Class Method Details

.from_h(hash) ⇒ Webhook

Parameters:

  • hash (Hash)

    string or symbol keys

Returns:



85
86
87
88
89
90
91
92
# File 'lib/inquirex/actions/webhook.rb', line 85

def self.from_h(hash)
  fetch = ->(key) { hash[key.to_s] || hash[key.to_sym] }
  new(
    url:     fetch.call(:url),
    headers: fetch.call(:headers) || {},
    timeout: fetch.call(:timeout) || DEFAULT_TIMEOUT
  )
end

Instance Method Details

#call(answers, _outbox) ⇒ Net::HTTPResponse

POSTs the answers envelope to the declared URL.

Parameters:

  • answers (Answers)

    completed answers

  • _outbox (Outbox)

    unused — webhooks build no messages

Returns:

  • (Net::HTTPResponse)

    the 2xx response

Raises:



53
54
55
56
57
58
59
60
# File 'lib/inquirex/actions/webhook.rb', line 53

def call(answers, _outbox)
  require "net/http"
  response = post(answers)
  code = response.code.to_i
  return response if (200..299).cover?(code)

  raise Errors::ActionError, "webhook #{host} responded with HTTP #{response.code}"
end

#hostString

Returns lowercase host of the webhook URL.

Returns:

  • (String)

    lowercase host of the webhook URL



45
# File 'lib/inquirex/actions/webhook.rb', line 45

def host = @uri.host.downcase

#to_hHash

Returns wire format, same shape .from_h accepts.

Returns:

  • (Hash)

    wire format, same shape .from_h accepts



76
77
78
79
80
81
# File 'lib/inquirex/actions/webhook.rb', line 76

def to_h
  hash = { "type" => "webhook", "url" => @url }
  hash["headers"] = @headers unless @headers.empty?
  hash["timeout"] = @timeout unless @timeout == DEFAULT_TIMEOUT
  hash
end

#validate_against(definition) ⇒ void

This method returns an undefined value.

Enforced from Definition#validate!.

Parameters:

  • definition (Definition)

    owning definition, source of allowed_domains

Raises:



67
68
69
70
71
72
73
# File 'lib/inquirex/actions/webhook.rb', line 67

def validate_against(definition)
  return if definition.allowed_host?(host)

  raise Errors::DefinitionError,
    "webhook url host #{host.inspect} is not covered by allowed_domains " \
    "#{definition.allowed_domains.inspect} — declare it at the top of the definition"
end