Skip to content

Commit 2a65217

Browse files
feat(client): add webhook support
1 parent cff7f1b commit 2a65217

10 files changed

Lines changed: 84 additions & 4 deletions

File tree

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ PATH
1414
imagekitio (4.2.0)
1515
cgi
1616
connection_pool
17+
standardwebhooks
1718

1819
GEM
1920
remote: https://rubygems.org/
@@ -143,6 +144,7 @@ GEM
143144
rexml (>= 3.2.6)
144145
sorbet-static-and-runtime (>= 0.5.10187)
145146
thor (>= 0.19.2)
147+
standardwebhooks (1.0.1)
146148
steep (1.10.0)
147149
activesupport (>= 5.1)
148150
concurrent-ruby (>= 1.1.10)

imagekitio.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
2626
s.extra_rdoc_files = ["README.md"]
2727
s.add_dependency "cgi"
2828
s.add_dependency "connection_pool"
29+
s.add_dependency "standardwebhooks"
2930
end

lib/imagekitio.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
# Gems.
3232
require "connection_pool"
33+
require "standardwebhooks"
3334

3435
# Package files.
3536
require_relative "imagekitio/version"

lib/imagekitio/client.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ class Client < Imagekitio::Internal::Transport::BaseClient
2525
# @return [String, nil]
2626
attr_reader :password
2727

28+
# Your ImageKit webhook secret for verifying webhook signatures (starts with
29+
# `whsec_`). You can find this in the
30+
# [ImageKit dashboard](https://imagekit.io/dashboard/developer/webhooks). Only
31+
# required if you're using webhooks.
32+
# @return [String, nil]
33+
attr_reader :webhook_secret
34+
2835
# @return [Imagekitio::Resources::CustomMetadataFields]
2936
attr_reader :custom_metadata_fields
3037

@@ -80,6 +87,11 @@ def base_url_overridden? = @base_url_overridden
8087
# dummy value. You can ignore this field. Defaults to
8188
# `ENV["OPTIONAL_IMAGEKIT_IGNORES_THIS"]`
8289
#
90+
# @param webhook_secret [String, nil] Your ImageKit webhook secret for verifying webhook signatures (starts with
91+
# `whsec_`). You can find this in the
92+
# [ImageKit dashboard](https://imagekit.io/dashboard/developer/webhooks). Only
93+
# required if you're using webhooks. Defaults to `ENV["IMAGEKIT_WEBHOOK_SECRET"]`
94+
#
8395
# @param base_url [String, nil] Override the default base URL for the API, e.g.,
8496
# `"https://api.example.com/v2/"`. Defaults to `ENV["IMAGE_KIT_BASE_URL"]`
8597
#
@@ -93,6 +105,7 @@ def base_url_overridden? = @base_url_overridden
93105
def initialize(
94106
private_key: ENV["IMAGEKIT_PRIVATE_KEY"],
95107
password: ENV.fetch("OPTIONAL_IMAGEKIT_IGNORES_THIS", "do_not_set"),
108+
webhook_secret: ENV["IMAGEKIT_WEBHOOK_SECRET"],
96109
base_url: ENV["IMAGE_KIT_BASE_URL"],
97110
max_retries: self.class::DEFAULT_MAX_RETRIES,
98111
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
@@ -109,6 +122,7 @@ def initialize(
109122

110123
@private_key = private_key.to_s
111124
@password = password.to_s
125+
@webhook_secret = webhook_secret&.to_s
112126

113127
super(
114128
base_url: base_url,

lib/imagekitio/resources/webhooks.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,18 @@ def unsafe_unwrap(payload)
1313

1414
# @param payload [String] The raw webhook payload as a string
1515
#
16+
# @param headers [Hash{String=>String}] The raw HTTP headers that came with the payload
17+
#
18+
# @param key [String, nil] The webhook signing key
19+
#
1620
# @return [Imagekitio::Models::VideoTransformationAcceptedEvent, Imagekitio::Models::VideoTransformationReadyEvent, Imagekitio::Models::VideoTransformationErrorEvent, Imagekitio::Models::UploadPreTransformSuccessEvent, Imagekitio::Models::UploadPreTransformErrorEvent, Imagekitio::Models::UploadPostTransformSuccessEvent, Imagekitio::Models::UploadPostTransformErrorEvent]
17-
def unwrap(payload)
21+
def unwrap(payload, headers:, key: @client.webhook_secret)
22+
if key.nil?
23+
raise ArgumentError.new("Cannot verify a webhook without a key on either the client's webhook_secret or passed in as an argument")
24+
end
25+
26+
::StandardWebhooks::Webhook.new(key).verify(payload, headers)
27+
1828
parsed = JSON.parse(payload, symbolize_names: true)
1929
Imagekitio::Internal::Type::Converter.coerce(Imagekitio::Models::UnwrapWebhookEvent, parsed)
2030
end

rbi/imagekitio/client.rbi

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ module Imagekitio
2020
sig { returns(T.nilable(String)) }
2121
attr_reader :password
2222

23+
# Your ImageKit webhook secret for verifying webhook signatures (starts with
24+
# `whsec_`). You can find this in the
25+
# [ImageKit dashboard](https://imagekit.io/dashboard/developer/webhooks). Only
26+
# required if you're using webhooks.
27+
sig { returns(T.nilable(String)) }
28+
attr_reader :webhook_secret
29+
2330
sig { returns(Imagekitio::Resources::CustomMetadataFields) }
2431
attr_reader :custom_metadata_fields
2532

@@ -65,6 +72,7 @@ module Imagekitio
6572
params(
6673
private_key: T.nilable(String),
6774
password: T.nilable(String),
75+
webhook_secret: T.nilable(String),
6876
base_url: T.nilable(String),
6977
max_retries: Integer,
7078
timeout: Float,
@@ -81,6 +89,11 @@ module Imagekitio
8189
# dummy value. You can ignore this field. Defaults to
8290
# `ENV["OPTIONAL_IMAGEKIT_IGNORES_THIS"]`
8391
password: ENV.fetch("OPTIONAL_IMAGEKIT_IGNORES_THIS", "do_not_set"),
92+
# Your ImageKit webhook secret for verifying webhook signatures (starts with
93+
# `whsec_`). You can find this in the
94+
# [ImageKit dashboard](https://imagekit.io/dashboard/developer/webhooks). Only
95+
# required if you're using webhooks. Defaults to `ENV["IMAGEKIT_WEBHOOK_SECRET"]`
96+
webhook_secret: ENV["IMAGEKIT_WEBHOOK_SECRET"],
8497
# Override the default base URL for the API, e.g.,
8598
# `"https://api.example.com/v2/"`. Defaults to `ENV["IMAGE_KIT_BASE_URL"]`
8699
base_url: ENV["IMAGE_KIT_BASE_URL"],

rbi/imagekitio/resources/webhooks.rbi

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ module Imagekitio
2323
end
2424

2525
sig do
26-
params(payload: String).returns(
26+
params(
27+
payload: String,
28+
headers: T::Hash[String, String],
29+
key: T.nilable(String)
30+
).returns(
2731
T.any(
2832
Imagekitio::VideoTransformationAcceptedEvent,
2933
Imagekitio::VideoTransformationReadyEvent,
@@ -37,7 +41,11 @@ module Imagekitio
3741
end
3842
def unwrap(
3943
# The raw webhook payload as a string
40-
payload
44+
payload,
45+
# The raw HTTP headers that came with the payload
46+
headers:,
47+
# The webhook signing key
48+
key: @client.webhook_secret
4149
)
4250
end
4351

sig/imagekitio/client.rbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module Imagekitio
1212

1313
attr_reader password: String?
1414

15+
attr_reader webhook_secret: String?
16+
1517
attr_reader custom_metadata_fields: Imagekitio::Resources::CustomMetadataFields
1618

1719
attr_reader files: Imagekitio::Resources::Files
@@ -39,6 +41,7 @@ module Imagekitio
3941
def initialize: (
4042
?private_key: String?,
4143
?password: String?,
44+
?webhook_secret: String?,
4245
?base_url: String?,
4346
?max_retries: Integer,
4447
?timeout: Float,

sig/imagekitio/resources/webhooks.rbs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ module Imagekitio
1212
| Imagekitio::UploadPostTransformErrorEvent)
1313

1414
def unwrap: (
15-
String payload
15+
String payload,
16+
headers: ::Hash[String, String],
17+
?key: String?
1618
) -> (Imagekitio::VideoTransformationAcceptedEvent
1719
| Imagekitio::VideoTransformationReadyEvent
1820
| Imagekitio::VideoTransformationErrorEvent

test/imagekitio/resources/webhooks_test.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,30 @@
33
require_relative "../test_helper"
44

55
class Imagekitio::Test::Resources::WebhooksTest < Imagekitio::Test::ResourceTest
6+
def test_unwrap
7+
key = "whsec_c2VjcmV0Cg=="
8+
9+
webhook = StandardWebhooks::Webhook.new(key)
10+
11+
data =
12+
"{\"id\":\"id\",\"type\":\"video.transformation.accepted\",\"created_at\":\"2019-12-27T18:11:19.117Z\",\"data\":{\"asset\":{\"url\":\"https://example.com\"},\"transformation\":{\"type\":\"video-transformation\",\"options\":{\"audio_codec\":\"aac\",\"auto_rotate\":true,\"format\":\"mp4\",\"quality\":0,\"stream_protocol\":\"HLS\",\"variants\":[\"string\"],\"video_codec\":\"h264\"}}},\"request\":{\"url\":\"https://example.com\",\"x_request_id\":\"x_request_id\",\"user_agent\":\"user_agent\"}}"
13+
message_id = "1"
14+
timestamp = Time.now.to_i.to_s
15+
signature = webhook.sign(message_id, timestamp, data)
16+
headers =
17+
{"webhook-id" => message_id, "webhook-timestamp" => timestamp, "webhook-signature" => signature}
18+
19+
@image_kit.webhooks.unwrap(data, headers: headers, key: key)
20+
21+
bad_headers = [
22+
headers.merge("webhook-id" => "bad"),
23+
headers.merge("webhook-timestamp" => "0"),
24+
headers.merge("webhook-signature" => webhook.sign(message_id, timestamp, "xxx"))
25+
]
26+
bad_headers.each do |bad_header|
27+
assert_raises(StandardWebhooks::WebhookVerificationError) do
28+
@image_kit.webhooks.unwrap(data, headers: bad_header, key: key)
29+
end
30+
end
31+
end
632
end

0 commit comments

Comments
 (0)