Class: Anvil::Webhook

Inherits:
Resources::Base show all
Defined in:
lib/anvil/resources/webhook.rb

Constant Summary collapse

ACTIONS =

Webhook actions/events

%w[
  weldCreate
  forgeComplete
  weldComplete
  signerComplete
  signerUpdateStatus
  etchPacketComplete
  documentGroupCreate
  webhookTest
].freeze

Instance Attribute Summary collapse

Attributes inherited from Resources::Base

#attributes, #client

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Resources::Base

#==, build_from_response, create, find, #inspect, list, #method_missing, #respond_to_missing?, #to_h, #to_json, with_client

Constructor Details

#initialize(payload:, token: nil, **options) ⇒ Webhook

Returns a new instance of Webhook.



21
22
23
24
25
26
27
28
29
30
31
# File 'lib/anvil/resources/webhook.rb', line 21

def initialize(payload:, token: nil, **options)
  @raw_payload = payload.is_a?(String) ? payload : payload.to_json
  @token = token

  begin
    parsed = JSON.parse(@raw_payload, symbolize_names: true)
    super(parsed, **options)
  rescue JSON::ParserError => e
    raise WebhookError, "Invalid webhook payload: #{e.message}"
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Anvil::Resources::Base

Instance Attribute Details

#raw_payloadObject (readonly)

Returns the value of attribute raw_payload.



19
20
21
# File 'lib/anvil/resources/webhook.rb', line 19

def raw_payload
  @raw_payload
end

#tokenObject (readonly)

Returns the value of attribute token.



19
20
21
# File 'lib/anvil/resources/webhook.rb', line 19

def token
  @token
end

Class Method Details

.create_test(action: 'webhookTest', data: {}) ⇒ Object

Create a test webhook for development



165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/anvil/resources/webhook.rb', line 165

def create_test(action: 'webhookTest', data: {})
  payload = {
    action: action,
    data: data,
    timestamp: Time.now.iso8601
  }

  new(
    payload: payload.to_json,
    token: Anvil.configuration.webhook_token
  )
end

.from_request(request) ⇒ Webhook

Verify and parse a webhook request

Parameters:

  • request (ActionDispatch::Request, Rack::Request)

    The incoming request

Returns:

  • (Webhook)

    The parsed and verified webhook



155
156
157
158
159
160
161
162
# File 'lib/anvil/resources/webhook.rb', line 155

def from_request(request)
  payload = request.body.read
  token = extract_token(request)

  webhook = new(payload: payload, token: token)
  webhook.valid!
  webhook
end

Instance Method Details

#actionObject



33
34
35
# File 'lib/anvil/resources/webhook.rb', line 33

def action
  attributes[:action]
end

#dataObject



37
38
39
# File 'lib/anvil/resources/webhook.rb', line 37

def data
  attributes[:data]
end

#decrypt(private_key_path = nil) ⇒ Object

Decrypt the webhook data (requires RSA private key)



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/anvil/resources/webhook.rb', line 69

def decrypt(private_key_path = nil)
  return data unless encrypted?

  private_key_path ||= ENV.fetch('ANVIL_RSA_PRIVATE_KEY_PATH', nil)

  unless private_key_path && File.exist?(private_key_path)
    raise WebhookError, 'Private key not found for decrypting webhook data'
  end

  begin
    private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
    encrypted_data = Base64.decode64(data)
    decrypted = private_key.private_decrypt(encrypted_data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
    JSON.parse(decrypted, symbolize_names: true)
  rescue StandardError => e
    raise WebhookError, "Failed to decrypt webhook data: #{e.message}"
  end
end

#document_group_created?Boolean

Returns:

  • (Boolean)


113
114
115
# File 'lib/anvil/resources/webhook.rb', line 113

def document_group_created?
  action == 'documentGroupCreate'
end

#encrypted?Boolean

Check if data is encrypted

Returns:

  • (Boolean)


64
65
66
# File 'lib/anvil/resources/webhook.rb', line 64

def encrypted?
  data.is_a?(String) && data.match?(%r{^[A-Za-z0-9+/=]+$})
end

#packet_eidObject



126
127
128
# File 'lib/anvil/resources/webhook.rb', line 126

def packet_eid
  data[:packetEid] if signature_packet_complete? || signer_complete?
end

#signature_packet_complete?Boolean

Returns:

  • (Boolean)


109
110
111
# File 'lib/anvil/resources/webhook.rb', line 109

def signature_packet_complete?
  action == 'etchPacketComplete'
end

#signer_complete?Boolean

Returns:

  • (Boolean)


101
102
103
# File 'lib/anvil/resources/webhook.rb', line 101

def signer_complete?
  action == 'signerComplete'
end

#signer_eidObject

Extract specific data based on webhook type



122
123
124
# File 'lib/anvil/resources/webhook.rb', line 122

def signer_eid
  data[:signerEid] if signer_complete? || signer_status_updated?
end

#signer_emailObject



146
147
148
# File 'lib/anvil/resources/webhook.rb', line 146

def signer_email
  data[:signerEmail] if signer_complete? || signer_status_updated?
end

#signer_nameObject



142
143
144
# File 'lib/anvil/resources/webhook.rb', line 142

def signer_name
  data[:signerName] if signer_complete? || signer_status_updated?
end

#signer_statusObject



138
139
140
# File 'lib/anvil/resources/webhook.rb', line 138

def signer_status
  data[:status] if signer_status_updated?
end

#signer_status_updated?Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/anvil/resources/webhook.rb', line 105

def signer_status_updated?
  action == 'signerUpdateStatus'
end

#test?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/anvil/resources/webhook.rb', line 117

def test?
  action == 'webhookTest'
end

#timestampObject



41
42
43
# File 'lib/anvil/resources/webhook.rb', line 41

def timestamp
  attributes[:timestamp] || attributes[:created_at]
end

#valid!Object



57
58
59
60
61
# File 'lib/anvil/resources/webhook.rb', line 57

def valid!
  raise WebhookVerificationError, 'Invalid webhook token' unless valid?

  true
end

#valid?(expected_token = nil) ⇒ Boolean

Verify the webhook token

Returns:

  • (Boolean)

Raises:



46
47
48
49
50
51
52
53
54
55
# File 'lib/anvil/resources/webhook.rb', line 46

def valid?(expected_token = nil)
  expected_token ||= Anvil.configuration.webhook_token

  raise WebhookVerificationError, 'No webhook token configured' if expected_token.nil? || expected_token.empty?

  return false unless token

  # Constant-time comparison to prevent timing attacks
  secure_compare(token, expected_token)
end

#webform_complete?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/anvil/resources/webhook.rb', line 93

def webform_complete?
  action == 'forgeComplete'
end

#webform_eidObject



134
135
136
# File 'lib/anvil/resources/webhook.rb', line 134

def webform_eid
  data[:forgeEid] if webform_complete?
end

#workflow_complete?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/anvil/resources/webhook.rb', line 97

def workflow_complete?
  action == 'weldComplete'
end

#workflow_created?Boolean

Helper methods for specific webhook types

Returns:

  • (Boolean)


89
90
91
# File 'lib/anvil/resources/webhook.rb', line 89

def workflow_created?
  action == 'weldCreate'
end

#workflow_eidObject



130
131
132
# File 'lib/anvil/resources/webhook.rb', line 130

def workflow_eid
  data[:weldEid] || data[:eid] if workflow_created? || workflow_complete?
end