kit-rb 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.githooks/pre-commit +21 -0
- data/.githooks/pre-push +6 -0
- data/CHANGELOG.md +83 -0
- data/README.md +58 -16
- data/docs/DESIGN.md +38 -14
- data/docs/TASKS.md +47 -0
- data/lib/kit/auth/api_key.rb +7 -0
- data/lib/kit/auth/credential.rb +22 -0
- data/lib/kit/auth/oauth.rb +7 -8
- data/lib/kit/connection.rb +53 -20
- data/lib/kit/errors.rb +52 -15
- data/lib/kit/oauth/token.rb +9 -0
- data/lib/kit/objects/account.rb +44 -3
- data/lib/kit/objects/broadcast_click.rb +17 -0
- data/lib/kit/objects/broadcast_stats.rb +30 -0
- data/lib/kit/objects/bulk_result.rb +39 -0
- data/lib/kit/objects/custom_field.rb +4 -2
- data/lib/kit/objects/email_stats.rb +25 -0
- data/lib/kit/objects/growth_stats.rb +20 -0
- data/lib/kit/objects/post.rb +4 -2
- data/lib/kit/objects/sequence_email.rb +5 -2
- data/lib/kit/objects/subscriber.rb +16 -9
- data/lib/kit/objects/subscriber_stats.rb +26 -0
- data/lib/kit/objects/tag.rb +5 -3
- data/lib/kit/objects/webhook_endpoint.rb +7 -2
- data/lib/kit/pagination.rb +47 -3
- data/lib/kit/resources/account.rb +9 -8
- data/lib/kit/resources/base.rb +46 -4
- data/lib/kit/resources/broadcasts.rb +39 -16
- data/lib/kit/resources/bulk.rb +18 -20
- data/lib/kit/resources/custom_fields.rb +2 -2
- data/lib/kit/resources/forms.rb +3 -3
- data/lib/kit/resources/posts.rb +1 -1
- data/lib/kit/resources/purchases.rb +10 -6
- data/lib/kit/resources/sequences.rb +49 -25
- data/lib/kit/resources/snippets.rb +13 -8
- data/lib/kit/resources/subscribers.rb +24 -11
- data/lib/kit/resources/tags.rb +6 -6
- data/lib/kit/resources/webhook_endpoints.rb +12 -4
- data/lib/kit/resources/webhooks.rb +1 -1
- data/lib/kit/version.rb +1 -1
- data/lib/kit/webhooks/delivery.rb +61 -0
- data/lib/kit/webhooks/events.rb +113 -0
- data/lib/kit/webhooks/signature.rb +100 -0
- data/lib/kit-rb.rb +10 -1
- data/sig/kit-rb.rbs +240 -37
- metadata +16 -2
- data/lib/kit/objects/raw.rb +0 -12
|
@@ -16,7 +16,7 @@ module Kit
|
|
|
16
16
|
|
|
17
17
|
# GET /v4/webhook_endpoints/:id
|
|
18
18
|
def get(id)
|
|
19
|
-
one(:get, "/v4/webhook_endpoints/#{id}", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
19
|
+
one(:get, "/v4/webhook_endpoints/#{path_id(id)}", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# POST /v4/webhook_endpoints
|
|
@@ -25,21 +25,29 @@ module Kit
|
|
|
25
25
|
body: { url: url, events: events, name: name, description: description }.compact)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# PATCH /v4/webhook_endpoints/:id — change name/url/description, pause or
|
|
29
|
+
# resume delivery with status: "active" | "disabled", or replace the
|
|
30
|
+
# subscribed events (the list given here replaces the whole set).
|
|
31
|
+
def update(id, name: nil, url: nil, description: nil, status: nil, events: nil)
|
|
32
|
+
body = { name: name, url: url, description: description, status: status, events: events }.compact
|
|
33
|
+
one(:patch, "/v4/webhook_endpoints/#{path_id(id)}", "webhook_endpoint", Objects::WebhookEndpoint, body: body)
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
# DELETE /v4/webhook_endpoints/:id
|
|
29
37
|
def delete(id)
|
|
30
|
-
http_delete("/v4/webhook_endpoints/#{id}")
|
|
38
|
+
http_delete("/v4/webhook_endpoints/#{path_id(id)}")
|
|
31
39
|
nil
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
# POST /v4/webhook_endpoints/:id/rotate_secret
|
|
35
43
|
def rotate_secret(id, force: nil)
|
|
36
|
-
one(:post, "/v4/webhook_endpoints/#{id}/rotate_secret", "webhook_endpoint", Objects::WebhookEndpoint,
|
|
44
|
+
one(:post, "/v4/webhook_endpoints/#{path_id(id)}/rotate_secret", "webhook_endpoint", Objects::WebhookEndpoint,
|
|
37
45
|
body: { force: force }.compact)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
# POST /v4/webhook_endpoints/:id/revoke_previous_secret
|
|
41
49
|
def revoke_previous_secret(id)
|
|
42
|
-
one(:post, "/v4/webhook_endpoints/#{id}/revoke_previous_secret", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
50
|
+
one(:post, "/v4/webhook_endpoints/#{path_id(id)}/revoke_previous_secret", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
43
51
|
end
|
|
44
52
|
end
|
|
45
53
|
end
|
data/lib/kit/version.rb
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Kit
|
|
6
|
+
module Webhooks
|
|
7
|
+
# One event inside a delivery. `id` is the deduplication key (deliveries
|
|
8
|
+
# are retried whole, so the same event can arrive twice); `type` is the
|
|
9
|
+
# event name (e.g. "subscriber.created"); `created` an ISO 8601 UTC string;
|
|
10
|
+
# `data` the event-specific payload, keyed by resource ("subscriber",
|
|
11
|
+
# "tag", ...), left as a Hash because its shape varies per type.
|
|
12
|
+
Event = Data.define(:id, :type, :created, :data) do
|
|
13
|
+
def self.from(hash)
|
|
14
|
+
new(id: hash["id"], type: hash["type"], created: hash["created"], data: hash["data"] || {})
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The JSON envelope a webhook endpoint receives (developers.kit.com/
|
|
19
|
+
# webhooks/delivery-format, verified 2026-09-06): { delivery_id, events[] }
|
|
20
|
+
# with 1..100 events of one type, plus the headers X-Kit-Delivery (the same
|
|
21
|
+
# id), X-Kit-Signature, and User-Agent "Kit-Webhooks/2.0". Answer 2xx to
|
|
22
|
+
# acknowledge; anything else is retried (8 attempts over ~41 hours).
|
|
23
|
+
#
|
|
24
|
+
# delivery = Kit::Webhooks::Delivery.from_request(
|
|
25
|
+
# request.raw_post, request.headers["X-Kit-Signature"], secret: secret
|
|
26
|
+
# )
|
|
27
|
+
# delivery.events.each { |event| handle(event) unless seen?(event.id) }
|
|
28
|
+
# The header carrying the delivery id on each request.
|
|
29
|
+
DELIVERY_HEADER = "X-Kit-Delivery"
|
|
30
|
+
|
|
31
|
+
Delivery = Data.define(:delivery_id, :events) do
|
|
32
|
+
def self.from(hash)
|
|
33
|
+
new(delivery_id: hash["delivery_id"], events: Array(hash["events"]).map { |event| Event.from(event) })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parses a raw JSON body. Raises UnexpectedResponseError when it is not
|
|
37
|
+
# the documented envelope.
|
|
38
|
+
def self.parse(payload)
|
|
39
|
+
body = JSON.parse(payload)
|
|
40
|
+
raise UnexpectedResponseError.new("webhook delivery is not a JSON object", body: body) unless body.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
from(body)
|
|
43
|
+
rescue JSON::ParserError => e
|
|
44
|
+
raise UnexpectedResponseError.new("webhook delivery is not valid JSON: #{e.message}", body: payload)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Verifies the signature, then parses. The one call a webhook receiver
|
|
48
|
+
# needs; a failed check raises SignatureError before any JSON is read.
|
|
49
|
+
def self.from_request(payload, signature_header, secret:, tolerance: Signature::DEFAULT_TOLERANCE,
|
|
50
|
+
now: Time.now.to_i)
|
|
51
|
+
Signature.verify!(payload, signature_header, secret: secret, tolerance: tolerance, now: now)
|
|
52
|
+
parse(payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Every event in a delivery shares one type.
|
|
56
|
+
def type
|
|
57
|
+
events.first&.type
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
module Webhooks
|
|
5
|
+
# Event types a webhook endpoint (/v4/webhook_endpoints) can subscribe to,
|
|
6
|
+
# as listed at developers.kit.com/webhooks/event-types (verified
|
|
7
|
+
# 2026-09-06). Use these instead of hand-typed strings:
|
|
8
|
+
#
|
|
9
|
+
# client.webhook_endpoints.create(url: url, events: [Events::SUBSCRIBER_CREATED, Events::TAG_CREATED])
|
|
10
|
+
#
|
|
11
|
+
# DATA_KEYS names the keys each event's `data` carries. PLANNED events are
|
|
12
|
+
# documented but not yet delivered.
|
|
13
|
+
module Events
|
|
14
|
+
SUBSCRIBER_CREATED = "subscriber.created"
|
|
15
|
+
SUBSCRIBER_ACTIVATED = "subscriber.activated"
|
|
16
|
+
SUBSCRIBER_UNSUBSCRIBED = "subscriber.unsubscribed"
|
|
17
|
+
SUBSCRIBER_BOUNCED = "subscriber.bounced"
|
|
18
|
+
SUBSCRIBER_COMPLAINED = "subscriber.complained"
|
|
19
|
+
SUBSCRIBER_SUBSCRIBED_TO_FORM = "subscriber.subscribed_to_form"
|
|
20
|
+
SUBSCRIBER_ADDED_TO_SEQUENCE = "subscriber.added_to_sequence"
|
|
21
|
+
SUBSCRIBER_SEQUENCE_COMPLETED = "subscriber.sequence_completed"
|
|
22
|
+
SUBSCRIBER_TAG_ADDED = "subscriber.tag_added"
|
|
23
|
+
SUBSCRIBER_TAG_REMOVED = "subscriber.tag_removed"
|
|
24
|
+
SUBSCRIBER_CUSTOM_FIELD_VALUE_UPDATED = "subscriber.custom_field_value_updated"
|
|
25
|
+
SUBSCRIBER_PRODUCT_PURCHASED = "subscriber.product_purchased" # planned
|
|
26
|
+
SUBSCRIBER_LINK_CLICKED = "subscriber.link_clicked" # planned
|
|
27
|
+
SUBSCRIBER_EMAIL_OPENED = "subscriber.email_opened" # planned
|
|
28
|
+
TAG_CREATED = "tag.created"
|
|
29
|
+
TAG_DELETED = "tag.deleted"
|
|
30
|
+
CUSTOM_FIELD_CREATED = "custom_field.created"
|
|
31
|
+
CUSTOM_FIELD_DELETED = "custom_field.deleted"
|
|
32
|
+
SEQUENCE_CREATED = "sequence.created"
|
|
33
|
+
SEQUENCE_DELETED = "sequence.deleted"
|
|
34
|
+
SEQUENCE_PUBLISHED = "sequence.published"
|
|
35
|
+
SEQUENCE_DISABLED = "sequence.disabled"
|
|
36
|
+
BROADCAST_CREATED = "broadcast.created"
|
|
37
|
+
BROADCAST_SENT = "broadcast.sent"
|
|
38
|
+
BROADCAST_DELETED = "broadcast.deleted"
|
|
39
|
+
POST_PUBLISHED = "post.published"
|
|
40
|
+
LANDING_PAGE_CREATED = "landing_page.created" # planned
|
|
41
|
+
LANDING_PAGE_DELETED = "landing_page.deleted" # planned
|
|
42
|
+
|
|
43
|
+
DATA_KEYS = {
|
|
44
|
+
SUBSCRIBER_CREATED => %w[subscriber], SUBSCRIBER_ACTIVATED => %w[subscriber],
|
|
45
|
+
SUBSCRIBER_UNSUBSCRIBED => %w[subscriber], SUBSCRIBER_BOUNCED => %w[subscriber],
|
|
46
|
+
SUBSCRIBER_COMPLAINED => %w[subscriber], SUBSCRIBER_SUBSCRIBED_TO_FORM => %w[subscriber form],
|
|
47
|
+
SUBSCRIBER_ADDED_TO_SEQUENCE => %w[subscriber sequence],
|
|
48
|
+
SUBSCRIBER_SEQUENCE_COMPLETED => %w[subscriber sequence],
|
|
49
|
+
SUBSCRIBER_TAG_ADDED => %w[subscriber tag], SUBSCRIBER_TAG_REMOVED => %w[subscriber tag],
|
|
50
|
+
SUBSCRIBER_CUSTOM_FIELD_VALUE_UPDATED => %w[subscriber custom_field],
|
|
51
|
+
SUBSCRIBER_PRODUCT_PURCHASED => %w[subscriber], SUBSCRIBER_LINK_CLICKED => %w[subscriber],
|
|
52
|
+
SUBSCRIBER_EMAIL_OPENED => %w[subscriber],
|
|
53
|
+
TAG_CREATED => %w[tag], TAG_DELETED => %w[tag],
|
|
54
|
+
CUSTOM_FIELD_CREATED => %w[custom_field], CUSTOM_FIELD_DELETED => %w[custom_field],
|
|
55
|
+
SEQUENCE_CREATED => %w[sequence], SEQUENCE_DELETED => %w[sequence],
|
|
56
|
+
SEQUENCE_PUBLISHED => %w[sequence], SEQUENCE_DISABLED => %w[sequence],
|
|
57
|
+
BROADCAST_CREATED => %w[broadcast], BROADCAST_SENT => %w[broadcast], BROADCAST_DELETED => %w[broadcast],
|
|
58
|
+
POST_PUBLISHED => %w[post],
|
|
59
|
+
LANDING_PAGE_CREATED => [], LANDING_PAGE_DELETED => []
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
PLANNED = [
|
|
63
|
+
SUBSCRIBER_PRODUCT_PURCHASED, SUBSCRIBER_LINK_CLICKED, SUBSCRIBER_EMAIL_OPENED,
|
|
64
|
+
LANDING_PAGE_CREATED, LANDING_PAGE_DELETED
|
|
65
|
+
].freeze
|
|
66
|
+
|
|
67
|
+
ALL = DATA_KEYS.keys.freeze
|
|
68
|
+
AVAILABLE = (ALL - PLANNED).freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Event names for the previous-generation /v4/webhooks (one event per
|
|
72
|
+
# webhook, unsigned). Kept for existing integrations; new ones should use
|
|
73
|
+
# webhook endpoints. REQUIRED_PARAM names the extra key the `event` hash
|
|
74
|
+
# must carry for the events that are scoped to a resource:
|
|
75
|
+
#
|
|
76
|
+
# client.webhooks.create(target_url: url,
|
|
77
|
+
# event: { name: LegacyEvents::TAG_ADD, tag_id: 12 })
|
|
78
|
+
module LegacyEvents
|
|
79
|
+
SUBSCRIBER_ACTIVATE = "subscriber.subscriber_activate"
|
|
80
|
+
SUBSCRIBER_UNSUBSCRIBE = "subscriber.subscriber_unsubscribe"
|
|
81
|
+
SUBSCRIBER_BOUNCE = "subscriber.subscriber_bounce"
|
|
82
|
+
SUBSCRIBER_COMPLAIN = "subscriber.subscriber_complain"
|
|
83
|
+
FORM_SUBSCRIBE = "subscriber.form_subscribe"
|
|
84
|
+
COURSE_SUBSCRIBE = "subscriber.course_subscribe"
|
|
85
|
+
COURSE_COMPLETE = "subscriber.course_complete"
|
|
86
|
+
LINK_CLICK = "subscriber.link_click"
|
|
87
|
+
PRODUCT_PURCHASE = "subscriber.product_purchase"
|
|
88
|
+
TAG_ADD = "subscriber.tag_add"
|
|
89
|
+
TAG_REMOVE = "subscriber.tag_remove"
|
|
90
|
+
PURCHASE_CREATE = "purchase.purchase_create"
|
|
91
|
+
FIELD_CREATED = "custom_field.field_created"
|
|
92
|
+
FIELD_DELETED = "custom_field.field_deleted"
|
|
93
|
+
FIELD_VALUE_UPDATED = "custom_field.field_value_updated"
|
|
94
|
+
|
|
95
|
+
REQUIRED_PARAM = {
|
|
96
|
+
FORM_SUBSCRIBE => :form_id,
|
|
97
|
+
COURSE_SUBSCRIBE => :sequence_id,
|
|
98
|
+
COURSE_COMPLETE => :sequence_id,
|
|
99
|
+
LINK_CLICK => :initiator_value,
|
|
100
|
+
PRODUCT_PURCHASE => :product_id,
|
|
101
|
+
TAG_ADD => :tag_id,
|
|
102
|
+
TAG_REMOVE => :tag_id,
|
|
103
|
+
FIELD_VALUE_UPDATED => :custom_field_id
|
|
104
|
+
}.freeze
|
|
105
|
+
|
|
106
|
+
ALL = [
|
|
107
|
+
SUBSCRIBER_ACTIVATE, SUBSCRIBER_UNSUBSCRIBE, SUBSCRIBER_BOUNCE, SUBSCRIBER_COMPLAIN,
|
|
108
|
+
FORM_SUBSCRIBE, COURSE_SUBSCRIBE, COURSE_COMPLETE, LINK_CLICK, PRODUCT_PURCHASE,
|
|
109
|
+
TAG_ADD, TAG_REMOVE, PURCHASE_CREATE, FIELD_CREATED, FIELD_DELETED, FIELD_VALUE_UPDATED
|
|
110
|
+
].freeze
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Kit
|
|
6
|
+
module Webhooks
|
|
7
|
+
# Raised when a delivery's X-Kit-Signature is missing, malformed, stale, or
|
|
8
|
+
# does not match the body under any supplied secret.
|
|
9
|
+
class SignatureError < Error; end
|
|
10
|
+
|
|
11
|
+
# Verifies the `X-Kit-Signature` header Kit sends with every webhook-endpoint
|
|
12
|
+
# delivery (verified against developers.kit.com/webhooks/verifying-signatures,
|
|
13
|
+
# 2026-09-06):
|
|
14
|
+
#
|
|
15
|
+
# X-Kit-Signature: t=1753797130,v1=<hex>[,v1=<hex>]
|
|
16
|
+
#
|
|
17
|
+
# Each v1 is hex(HMAC-SHA256(secret, "#{t}.#{raw_body}")). During a secret
|
|
18
|
+
# rotation the header carries two v1 entries (new and previous secret), so
|
|
19
|
+
# a delivery is valid when any v1 matches any of the secrets you hold.
|
|
20
|
+
#
|
|
21
|
+
# Kit::Webhooks::Signature.verify!(request.raw_post, request.headers["X-Kit-Signature"],
|
|
22
|
+
# secret: ENV["KIT_WEBHOOK_SECRET"])
|
|
23
|
+
module Signature
|
|
24
|
+
HEADER = "X-Kit-Signature"
|
|
25
|
+
SCHEME = "v1"
|
|
26
|
+
DEFAULT_TOLERANCE = 300 # seconds; Kit's recommended replay window
|
|
27
|
+
|
|
28
|
+
Parsed = Data.define(:timestamp, :signatures)
|
|
29
|
+
|
|
30
|
+
# hex(HMAC-SHA256(secret, "t.payload")) — what Kit puts in v1.
|
|
31
|
+
def self.compute(secret, timestamp, payload)
|
|
32
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{payload}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Splits the header into its timestamp and v1 signatures. Pairs are kept
|
|
36
|
+
# as a list, not a Hash: a rotation sends two v1 entries.
|
|
37
|
+
def self.parse(header)
|
|
38
|
+
raise SignatureError, "missing #{HEADER} header" if header.nil? || header.strip.empty?
|
|
39
|
+
|
|
40
|
+
pairs = header.split(",").map { |pair| pair.strip.split("=", 2) }
|
|
41
|
+
signatures = pairs.filter_map { |key, value| value if key == SCHEME }
|
|
42
|
+
raise SignatureError, "malformed #{HEADER} header: #{header.inspect}" if signatures.empty?
|
|
43
|
+
|
|
44
|
+
Parsed.new(timestamp: parse_timestamp(pairs, header), signatures: signatures)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.parse_timestamp(pairs, header)
|
|
48
|
+
value = pairs.find { |key, _| key == "t" }&.last
|
|
49
|
+
raise SignatureError, "malformed #{HEADER} header: #{header.inspect}" if value.nil?
|
|
50
|
+
raise SignatureError, "malformed timestamp in #{HEADER}: #{value.inspect}" unless value.match?(/\A\d+\z/)
|
|
51
|
+
|
|
52
|
+
value.to_i
|
|
53
|
+
end
|
|
54
|
+
private_class_method :parse_timestamp
|
|
55
|
+
|
|
56
|
+
# True when the payload was signed by one of `secret` (a String, or an
|
|
57
|
+
# Array of Strings while you hold both sides of a rotation) within
|
|
58
|
+
# `tolerance` seconds of `now`. Constant-time comparison.
|
|
59
|
+
def self.verify?(payload, header, secret:, tolerance: DEFAULT_TOLERANCE, now: Time.now.to_i)
|
|
60
|
+
verify!(payload, header, secret: secret, tolerance: tolerance, now: now)
|
|
61
|
+
true
|
|
62
|
+
rescue SignatureError
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# As #verify?, but raises SignatureError describing why the delivery was
|
|
67
|
+
# rejected. `payload` must be the raw request body, byte for byte.
|
|
68
|
+
def self.verify!(payload, header, secret:, tolerance: DEFAULT_TOLERANCE, now: Time.now.to_i)
|
|
69
|
+
secrets = secrets_from(secret)
|
|
70
|
+
parsed = parse(header)
|
|
71
|
+
check_freshness(parsed.timestamp, tolerance, now)
|
|
72
|
+
return true if secrets.any? { |value| matches?(value, parsed, payload) }
|
|
73
|
+
|
|
74
|
+
raise SignatureError, "no #{SCHEME} signature matched the payload"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.secrets_from(secret)
|
|
78
|
+
secrets = Array(secret).reject { |value| value.nil? || value.empty? }
|
|
79
|
+
raise ArgumentError, "at least one webhook secret is required" if secrets.empty?
|
|
80
|
+
|
|
81
|
+
secrets
|
|
82
|
+
end
|
|
83
|
+
private_class_method :secrets_from
|
|
84
|
+
|
|
85
|
+
def self.check_freshness(timestamp, tolerance, now)
|
|
86
|
+
return unless tolerance
|
|
87
|
+
|
|
88
|
+
age = (now - timestamp).abs
|
|
89
|
+
raise SignatureError, "timestamp outside tolerance (#{age}s > #{tolerance}s)" if age > tolerance
|
|
90
|
+
end
|
|
91
|
+
private_class_method :check_freshness
|
|
92
|
+
|
|
93
|
+
def self.matches?(secret, parsed, payload)
|
|
94
|
+
expected = compute(secret, parsed.timestamp, payload)
|
|
95
|
+
parsed.signatures.any? { |given| OpenSSL.secure_compare(expected, given) }
|
|
96
|
+
end
|
|
97
|
+
private_class_method :matches?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/kit-rb.rb
CHANGED
|
@@ -15,6 +15,7 @@ end
|
|
|
15
15
|
require "kit/version"
|
|
16
16
|
require "kit/errors"
|
|
17
17
|
require "kit/configuration"
|
|
18
|
+
require "kit/auth/credential"
|
|
18
19
|
require "kit/auth/api_key"
|
|
19
20
|
require "kit/auth/oauth"
|
|
20
21
|
require "kit/connection"
|
|
@@ -22,7 +23,6 @@ require "kit/pagination"
|
|
|
22
23
|
require "kit/oauth/pkce"
|
|
23
24
|
require "kit/oauth/token"
|
|
24
25
|
require "kit/oauth/client"
|
|
25
|
-
require "kit/objects/raw"
|
|
26
26
|
require "kit/objects/account"
|
|
27
27
|
require "kit/objects/creator_profile"
|
|
28
28
|
require "kit/objects/subscriber"
|
|
@@ -32,6 +32,11 @@ require "kit/objects/form"
|
|
|
32
32
|
require "kit/objects/sequence"
|
|
33
33
|
require "kit/objects/sequence_email"
|
|
34
34
|
require "kit/objects/broadcast"
|
|
35
|
+
require "kit/objects/broadcast_stats"
|
|
36
|
+
require "kit/objects/broadcast_click"
|
|
37
|
+
require "kit/objects/subscriber_stats"
|
|
38
|
+
require "kit/objects/email_stats"
|
|
39
|
+
require "kit/objects/growth_stats"
|
|
35
40
|
require "kit/objects/webhook"
|
|
36
41
|
require "kit/objects/webhook_endpoint"
|
|
37
42
|
require "kit/objects/email_template"
|
|
@@ -39,6 +44,10 @@ require "kit/objects/segment"
|
|
|
39
44
|
require "kit/objects/post"
|
|
40
45
|
require "kit/objects/snippet"
|
|
41
46
|
require "kit/objects/purchase"
|
|
47
|
+
require "kit/objects/bulk_result"
|
|
48
|
+
require "kit/webhooks/signature"
|
|
49
|
+
require "kit/webhooks/delivery"
|
|
50
|
+
require "kit/webhooks/events"
|
|
42
51
|
require "kit/resources/base"
|
|
43
52
|
require "kit/resources/account"
|
|
44
53
|
require "kit/resources/subscribers"
|