vortex-ruby-sdk 1.8.4 → 1.9.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/README.md +44 -0
- data/lib/vortex/version.rb +1 -1
- data/lib/vortex/webhook_types.rb +111 -0
- data/lib/vortex/webhooks.rb +75 -0
- data/lib/vortex.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8665551bcdf4cf5b7ab05fc5b2dd880b9e1166dd4f544acceb864365e86c8db
|
|
4
|
+
data.tar.gz: 82d89462684d03e6b26769f20b8a44ba3f4fe71cf729ce78f54268f070fa36df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32a6d0a546f91c5165f43af6ac0ed995386b92d267006eb6793b865676fe0bcfe6631e748294ecf589253d7621df492aff7b442881266f7906dc6833b78be63e
|
|
7
|
+
data.tar.gz: 39253da9f792aacc194e619eb4ab00104d15ce7901ca5345a82e1681f3aa2d0e0bb268c6b832fad3537736641378f57fe3ee2ae31a5af8b2157ea5a8a2b02c17
|
data/README.md
CHANGED
|
@@ -262,6 +262,50 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
262
262
|
|
|
263
263
|
## Contributing
|
|
264
264
|
|
|
265
|
+
## Webhooks
|
|
266
|
+
|
|
267
|
+
The SDK provides built-in support for verifying and parsing incoming webhook events from Vortex.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
require 'vortex'
|
|
271
|
+
|
|
272
|
+
webhooks = Vortex::Webhooks.new(secret: ENV['VORTEX_WEBHOOK_SECRET'])
|
|
273
|
+
|
|
274
|
+
# In your HTTP handler (Rails example):
|
|
275
|
+
class WebhooksController < ApplicationController
|
|
276
|
+
skip_before_action :verify_authenticity_token
|
|
277
|
+
|
|
278
|
+
def create
|
|
279
|
+
payload = request.body.read
|
|
280
|
+
signature = request.headers['X-Vortex-Signature']
|
|
281
|
+
|
|
282
|
+
begin
|
|
283
|
+
event = webhooks.construct_event(payload, signature)
|
|
284
|
+
rescue Vortex::WebhookSignatureError
|
|
285
|
+
head :bad_request
|
|
286
|
+
return
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
case event
|
|
290
|
+
when Vortex::WebhookEvent
|
|
291
|
+
Rails.logger.info "Webhook event: #{event.type}"
|
|
292
|
+
when Vortex::AnalyticsEvent
|
|
293
|
+
Rails.logger.info "Analytics event: #{event.name}"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
head :ok
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Event Type Constants
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
if event.type == Vortex::WebhookEventTypes::INVITATION_ACCEPTED
|
|
305
|
+
# Handle invitation accepted
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
265
309
|
Bug reports and pull requests are welcome on GitHub at https://github.com/vortexsoftware/vortex-ruby-sdk.
|
|
266
310
|
|
|
267
311
|
## License
|
data/lib/vortex/version.rb
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vortex
|
|
4
|
+
# Webhook event type constants
|
|
5
|
+
module WebhookEventTypes
|
|
6
|
+
# Invitation Lifecycle
|
|
7
|
+
INVITATION_CREATED = 'invitation.created'
|
|
8
|
+
INVITATION_ACCEPTED = 'invitation.accepted'
|
|
9
|
+
INVITATION_DEACTIVATED = 'invitation.deactivated'
|
|
10
|
+
INVITATION_EMAIL_DELIVERED = 'invitation.email.delivered'
|
|
11
|
+
INVITATION_EMAIL_BOUNCED = 'invitation.email.bounced'
|
|
12
|
+
INVITATION_EMAIL_OPENED = 'invitation.email.opened'
|
|
13
|
+
INVITATION_LINK_CLICKED = 'invitation.link.clicked'
|
|
14
|
+
INVITATION_REMINDER_SENT = 'invitation.reminder.sent'
|
|
15
|
+
|
|
16
|
+
# Deployment Lifecycle
|
|
17
|
+
DEPLOYMENT_CREATED = 'deployment.created'
|
|
18
|
+
DEPLOYMENT_DEACTIVATED = 'deployment.deactivated'
|
|
19
|
+
|
|
20
|
+
# A/B Testing
|
|
21
|
+
ABTEST_STARTED = 'abtest.started'
|
|
22
|
+
ABTEST_WINNER_DECLARED = 'abtest.winner_declared'
|
|
23
|
+
|
|
24
|
+
# Member/Group
|
|
25
|
+
MEMBER_CREATED = 'member.created'
|
|
26
|
+
GROUP_MEMBER_ADDED = 'group.member.added'
|
|
27
|
+
|
|
28
|
+
# Email
|
|
29
|
+
EMAIL_COMPLAINED = 'email.complained'
|
|
30
|
+
|
|
31
|
+
ALL = [
|
|
32
|
+
INVITATION_CREATED, INVITATION_ACCEPTED, INVITATION_DEACTIVATED,
|
|
33
|
+
INVITATION_EMAIL_DELIVERED, INVITATION_EMAIL_BOUNCED, INVITATION_EMAIL_OPENED,
|
|
34
|
+
INVITATION_LINK_CLICKED, INVITATION_REMINDER_SENT,
|
|
35
|
+
DEPLOYMENT_CREATED, DEPLOYMENT_DEACTIVATED,
|
|
36
|
+
ABTEST_STARTED, ABTEST_WINNER_DECLARED,
|
|
37
|
+
MEMBER_CREATED, GROUP_MEMBER_ADDED,
|
|
38
|
+
EMAIL_COMPLAINED
|
|
39
|
+
].freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Analytics event type constants
|
|
43
|
+
module AnalyticsEventTypes
|
|
44
|
+
WIDGET_LOADED = 'widget_loaded'
|
|
45
|
+
INVITATION_SENT = 'invitation_sent'
|
|
46
|
+
INVITATION_CLICKED = 'invitation_clicked'
|
|
47
|
+
INVITATION_ACCEPTED = 'invitation_accepted'
|
|
48
|
+
SHARE_TRIGGERED = 'share_triggered'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A Vortex webhook event representing a server-side state change
|
|
52
|
+
#
|
|
53
|
+
# @attr_reader id [String] Unique event ID
|
|
54
|
+
# @attr_reader type [String] The semantic event type
|
|
55
|
+
# @attr_reader timestamp [String] ISO-8601 timestamp
|
|
56
|
+
# @attr_reader account_id [String] The account ID
|
|
57
|
+
# @attr_reader environment_id [String, nil] The environment ID
|
|
58
|
+
# @attr_reader source_table [String] The source table
|
|
59
|
+
# @attr_reader operation [String] The database operation
|
|
60
|
+
# @attr_reader data [Hash] Event-specific payload data
|
|
61
|
+
class WebhookEvent
|
|
62
|
+
attr_reader :id, :type, :timestamp, :account_id, :environment_id,
|
|
63
|
+
:source_table, :operation, :data
|
|
64
|
+
|
|
65
|
+
def initialize(attrs)
|
|
66
|
+
@id = attrs['id']
|
|
67
|
+
@type = attrs['type']
|
|
68
|
+
@timestamp = attrs['timestamp']
|
|
69
|
+
@account_id = attrs['accountId']
|
|
70
|
+
@environment_id = attrs['environmentId']
|
|
71
|
+
@source_table = attrs['sourceTable']
|
|
72
|
+
@operation = attrs['operation']
|
|
73
|
+
@data = attrs['data'] || {}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# An analytics event representing client-side behavioral telemetry
|
|
78
|
+
class AnalyticsEvent
|
|
79
|
+
attr_reader :id, :name, :account_id, :organization_id, :project_id,
|
|
80
|
+
:environment_id, :deployment_id, :widget_configuration_id,
|
|
81
|
+
:foreign_user_id, :session_id, :payload, :platform,
|
|
82
|
+
:segmentation, :timestamp
|
|
83
|
+
|
|
84
|
+
def initialize(attrs)
|
|
85
|
+
@id = attrs['id']
|
|
86
|
+
@name = attrs['name']
|
|
87
|
+
@account_id = attrs['accountId']
|
|
88
|
+
@organization_id = attrs['organizationId']
|
|
89
|
+
@project_id = attrs['projectId']
|
|
90
|
+
@environment_id = attrs['environmentId']
|
|
91
|
+
@deployment_id = attrs['deploymentId']
|
|
92
|
+
@widget_configuration_id = attrs['widgetConfigurationId']
|
|
93
|
+
@foreign_user_id = attrs['foreignUserId']
|
|
94
|
+
@session_id = attrs['sessionId']
|
|
95
|
+
@payload = attrs['payload']
|
|
96
|
+
@platform = attrs['platform']
|
|
97
|
+
@segmentation = attrs['segmentation']
|
|
98
|
+
@timestamp = attrs['timestamp']
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns true if the parsed hash is a webhook event
|
|
103
|
+
def self.webhook_event?(parsed)
|
|
104
|
+
parsed.key?('type') && !parsed.key?('name')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns true if the parsed hash is an analytics event
|
|
108
|
+
def self.analytics_event?(parsed)
|
|
109
|
+
parsed.key?('name')
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Vortex
|
|
7
|
+
# Error raised when webhook signature verification fails
|
|
8
|
+
class WebhookSignatureError < VortexError
|
|
9
|
+
def initialize(message = nil)
|
|
10
|
+
super(message || 'Webhook signature verification failed. Ensure you are using the raw request body and the correct signing secret.')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Core webhook verification and parsing.
|
|
15
|
+
#
|
|
16
|
+
# This class is framework-agnostic — use it directly or with
|
|
17
|
+
# the Rails/Sinatra framework integrations.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# webhooks = Vortex::Webhooks.new(secret: ENV['VORTEX_WEBHOOK_SECRET'])
|
|
21
|
+
# event = webhooks.construct_event(request.body.read, request.env['HTTP_X_VORTEX_SIGNATURE'])
|
|
22
|
+
class Webhooks
|
|
23
|
+
# @param secret [String] The webhook signing secret from your Vortex dashboard
|
|
24
|
+
def initialize(secret:)
|
|
25
|
+
raise ArgumentError, 'Vortex::Webhooks requires a secret' if secret.nil? || secret.empty?
|
|
26
|
+
|
|
27
|
+
@secret = secret
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Verify the HMAC-SHA256 signature of an incoming webhook payload.
|
|
31
|
+
#
|
|
32
|
+
# @param payload [String] The raw request body
|
|
33
|
+
# @param signature [String] The value of the X-Vortex-Signature header
|
|
34
|
+
# @return [Boolean] true if the signature is valid
|
|
35
|
+
def verify_signature(payload, signature)
|
|
36
|
+
return false if signature.nil? || signature.empty?
|
|
37
|
+
|
|
38
|
+
expected = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload)
|
|
39
|
+
|
|
40
|
+
# Timing-safe comparison to prevent timing attacks
|
|
41
|
+
secure_compare(signature, expected)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Verify and parse an incoming webhook payload.
|
|
45
|
+
#
|
|
46
|
+
# @param payload [String] The raw request body
|
|
47
|
+
# @param signature [String] The value of the X-Vortex-Signature header
|
|
48
|
+
# @return [WebhookEvent, AnalyticsEvent] A typed event object
|
|
49
|
+
# @raise [WebhookSignatureError] If the signature is invalid
|
|
50
|
+
def construct_event(payload, signature)
|
|
51
|
+
raise WebhookSignatureError unless verify_signature(payload, signature)
|
|
52
|
+
|
|
53
|
+
parsed = JSON.parse(payload)
|
|
54
|
+
|
|
55
|
+
if Vortex.webhook_event?(parsed)
|
|
56
|
+
WebhookEvent.new(parsed)
|
|
57
|
+
elsif Vortex.analytics_event?(parsed)
|
|
58
|
+
AnalyticsEvent.new(parsed)
|
|
59
|
+
else
|
|
60
|
+
WebhookEvent.new(parsed)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Timing-safe string comparison
|
|
67
|
+
def secure_compare(a, b)
|
|
68
|
+
return false unless a.bytesize == b.bytesize
|
|
69
|
+
|
|
70
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
71
|
+
rescue StandardError
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/vortex.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vortex-ruby-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vortex Software
|
|
@@ -160,6 +160,8 @@ files:
|
|
|
160
160
|
- lib/vortex/sinatra.rb
|
|
161
161
|
- lib/vortex/types.rb
|
|
162
162
|
- lib/vortex/version.rb
|
|
163
|
+
- lib/vortex/webhook_types.rb
|
|
164
|
+
- lib/vortex/webhooks.rb
|
|
163
165
|
- vortex-ruby-sdk.gemspec
|
|
164
166
|
homepage: https://github.com/vortexsoftware/vortex-ruby-sdk
|
|
165
167
|
licenses:
|