hooksniff 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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -1
  3. data/lib/openapi_client/api/admin_api.rb +452 -0
  4. data/lib/openapi_client/api/alerts_api.rb +322 -0
  5. data/lib/openapi_client/api/analytics_api.rb +208 -0
  6. data/lib/openapi_client/api/api_keys_api.rb +252 -0
  7. data/lib/openapi_client/api/audit_log_api.rb +140 -0
  8. data/lib/openapi_client/api/auth_api.rb +1080 -0
  9. data/lib/openapi_client/api/billing_api.rb +500 -0
  10. data/lib/openapi_client/api/contact_api.rb +88 -0
  11. data/lib/openapi_client/api/custom_domains_api.rb +253 -0
  12. data/lib/openapi_client/api/customer_portal_api.rb +700 -0
  13. data/lib/openapi_client/api/delivery_details_api.rb +146 -0
  14. data/lib/openapi_client/api/devices_api.rb +202 -0
  15. data/lib/openapi_client/api/embed_api.rb +128 -0
  16. data/lib/openapi_client/api/endpoints_api.rb +468 -0
  17. data/lib/openapi_client/api/events_api.rb +75 -0
  18. data/lib/openapi_client/api/health_api.rb +193 -0
  19. data/lib/openapi_client/api/inbound_api.rb +170 -0
  20. data/lib/openapi_client/api/notifications_api.rb +309 -0
  21. data/lib/openapi_client/api/o_auth_api.rb +181 -0
  22. data/lib/openapi_client/api/outbound_ips_api.rb +77 -0
  23. data/lib/openapi_client/api/playground_api.rb +143 -0
  24. data/lib/openapi_client/api/rate_limits_api.rb +252 -0
  25. data/lib/openapi_client/api/routing_api.rb +393 -0
  26. data/lib/openapi_client/api/schemas_api.rb +268 -0
  27. data/lib/openapi_client/api/search_api.rb +96 -0
  28. data/lib/openapi_client/api/simulator_api.rb +82 -0
  29. data/lib/openapi_client/api/sso_api.rb +241 -0
  30. data/lib/openapi_client/api/stats_api.rb +77 -0
  31. data/lib/openapi_client/api/stream_api.rb +88 -0
  32. data/lib/openapi_client/api/teams_api.rb +476 -0
  33. data/lib/openapi_client/api/templates_api.rb +213 -0
  34. data/lib/openapi_client/api/transforms_api.rb +368 -0
  35. data/lib/openapi_client/api/webhooks_api.rb +534 -0
  36. data/lib/openapi_client/api_client.rb +397 -0
  37. data/lib/openapi_client/api_error.rb +58 -0
  38. data/lib/openapi_client/api_model_base.rb +88 -0
  39. data/lib/openapi_client/configuration.rb +312 -0
  40. data/lib/openapi_client/models/admin_revenue_get200_response_inner.rb +165 -0
  41. data/lib/openapi_client/models/admin_sdk_update_post_request.rb +156 -0
  42. data/lib/openapi_client/models/admin_users_id_plan_put_request.rb +181 -0
  43. data/lib/openapi_client/models/admin_users_id_status_put_request.rb +147 -0
  44. data/lib/openapi_client/models/alert_rule.rb +237 -0
  45. data/lib/openapi_client/models/api_key_info.rb +185 -0
  46. data/lib/openapi_client/models/apply_template_request.rb +173 -0
  47. data/lib/openapi_client/models/apply_template_response.rb +156 -0
  48. data/lib/openapi_client/models/auth2fa_enable_post200_response.rb +156 -0
  49. data/lib/openapi_client/models/auth_login_post200_response.rb +104 -0
  50. data/lib/openapi_client/models/auth_response.rb +167 -0
  51. data/lib/openapi_client/models/batch_replay_request.rb +166 -0
  52. data/lib/openapi_client/models/batch_response.rb +160 -0
  53. data/lib/openapi_client/models/batch_response_errors_inner.rb +156 -0
  54. data/lib/openapi_client/models/batch_webhook_request.rb +166 -0
  55. data/lib/openapi_client/models/billing_portal_post200_response.rb +147 -0
  56. data/lib/openapi_client/models/change_password_request.rb +199 -0
  57. data/lib/openapi_client/models/change_role_request.rb +188 -0
  58. data/lib/openapi_client/models/confirm2fa_request.rb +182 -0
  59. data/lib/openapi_client/models/contact_request.rb +242 -0
  60. data/lib/openapi_client/models/contact_response.rb +156 -0
  61. data/lib/openapi_client/models/create_alert_request.rb +277 -0
  62. data/lib/openapi_client/models/create_api_key_response.rb +175 -0
  63. data/lib/openapi_client/models/create_endpoint_request.rb +288 -0
  64. data/lib/openapi_client/models/create_team_request.rb +164 -0
  65. data/lib/openapi_client/models/create_transform_rule_request.rb +216 -0
  66. data/lib/openapi_client/models/create_webhook_request.rb +201 -0
  67. data/lib/openapi_client/models/custom_domains_post_request.rb +147 -0
  68. data/lib/openapi_client/models/customer_response.rb +256 -0
  69. data/lib/openapi_client/models/delivery.rb +246 -0
  70. data/lib/openapi_client/models/delivery_attempt.rb +205 -0
  71. data/lib/openapi_client/models/delivery_list_response.rb +176 -0
  72. data/lib/openapi_client/models/delivery_trend_response.rb +158 -0
  73. data/lib/openapi_client/models/delivery_trend_response_buckets_inner.rb +174 -0
  74. data/lib/openapi_client/models/device_token_response.rb +174 -0
  75. data/lib/openapi_client/models/disable2fa_request.rb +164 -0
  76. data/lib/openapi_client/models/enable2fa_request.rb +164 -0
  77. data/lib/openapi_client/models/endpoint.rb +321 -0
  78. data/lib/openapi_client/models/endpoint_health.rb +183 -0
  79. data/lib/openapi_client/models/endpoints_endpoint_id_transforms_test_post_request.rb +156 -0
  80. data/lib/openapi_client/models/endpoints_id_rotate_secret_post200_response.rb +156 -0
  81. data/lib/openapi_client/models/error.rb +165 -0
  82. data/lib/openapi_client/models/forgot_password_request.rb +164 -0
  83. data/lib/openapi_client/models/invite_request.rb +207 -0
  84. data/lib/openapi_client/models/invoice_response.rb +183 -0
  85. data/lib/openapi_client/models/latency_trend_response.rb +167 -0
  86. data/lib/openapi_client/models/latency_trend_response_buckets_inner.rb +165 -0
  87. data/lib/openapi_client/models/login_request.rb +190 -0
  88. data/lib/openapi_client/models/notification.rb +193 -0
  89. data/lib/openapi_client/models/notification_list_response.rb +167 -0
  90. data/lib/openapi_client/models/notification_preferences.rb +201 -0
  91. data/lib/openapi_client/models/notifications_unread_count_get200_response.rb +147 -0
  92. data/lib/openapi_client/models/outbound_ips_response.rb +158 -0
  93. data/lib/openapi_client/models/paginated_users.rb +176 -0
  94. data/lib/openapi_client/models/playground_get200_response.rb +160 -0
  95. data/lib/openapi_client/models/portal_notifications_put200_response.rb +156 -0
  96. data/lib/openapi_client/models/portal_profile.rb +184 -0
  97. data/lib/openapi_client/models/refresh_token_request.rb +164 -0
  98. data/lib/openapi_client/models/register_device_request.rb +208 -0
  99. data/lib/openapi_client/models/register_request.rb +201 -0
  100. data/lib/openapi_client/models/register_schema_request.rb +191 -0
  101. data/lib/openapi_client/models/resend_verification_request.rb +164 -0
  102. data/lib/openapi_client/models/reset_password_request.rb +199 -0
  103. data/lib/openapi_client/models/retry_policy.rb +216 -0
  104. data/lib/openapi_client/models/routing_info.rb +193 -0
  105. data/lib/openapi_client/models/search_result.rb +158 -0
  106. data/lib/openapi_client/models/simulator_post_request.rb +165 -0
  107. data/lib/openapi_client/models/sso_config_post_request.rb +190 -0
  108. data/lib/openapi_client/models/stats_response.rb +210 -0
  109. data/lib/openapi_client/models/stream_params.rb +201 -0
  110. data/lib/openapi_client/models/subscription_response.rb +201 -0
  111. data/lib/openapi_client/models/success_rate_response.rb +183 -0
  112. data/lib/openapi_client/models/system_stats.rb +185 -0
  113. data/lib/openapi_client/models/system_stats_plan_breakdown_inner.rb +156 -0
  114. data/lib/openapi_client/models/system_status.rb +210 -0
  115. data/lib/openapi_client/models/system_status_components_inner.rb +184 -0
  116. data/lib/openapi_client/models/team.rb +165 -0
  117. data/lib/openapi_client/models/team_detail_response.rb +169 -0
  118. data/lib/openapi_client/models/team_invite.rb +174 -0
  119. data/lib/openapi_client/models/team_member.rb +193 -0
  120. data/lib/openapi_client/models/test_webhook_request.rb +199 -0
  121. data/lib/openapi_client/models/test_webhook_response.rb +174 -0
  122. data/lib/openapi_client/models/transform_rule.rb +201 -0
  123. data/lib/openapi_client/models/two_factor_required_response.rb +165 -0
  124. data/lib/openapi_client/models/update_endpoint_request.rb +278 -0
  125. data/lib/openapi_client/models/update_notification_preferences.rb +195 -0
  126. data/lib/openapi_client/models/update_profile_request.rb +190 -0
  127. data/lib/openapi_client/models/update_routing_request.rb +190 -0
  128. data/lib/openapi_client/models/upgrade_request.rb +209 -0
  129. data/lib/openapi_client/models/upgrade_response.rb +166 -0
  130. data/lib/openapi_client/models/usage_response.rb +201 -0
  131. data/lib/openapi_client/models/user_summary.rb +193 -0
  132. data/lib/openapi_client/models/validate_event_request.rb +164 -0
  133. data/lib/openapi_client/models/verify2fa_request.rb +208 -0
  134. data/lib/openapi_client/models/verify_email_request.rb +164 -0
  135. data/lib/openapi_client/models/webhook_template.rb +183 -0
  136. data/lib/openapi_client/version.rb +15 -0
  137. data/lib/openapi_client.rb +169 -0
  138. metadata +139 -10
  139. data/lib/hooksniff/client.rb +0 -213
  140. data/lib/hooksniff/errors.rb +0 -43
  141. data/lib/hooksniff/models.rb +0 -136
  142. data/lib/hooksniff/verification.rb +0 -134
  143. data/lib/hooksniff/version.rb +0 -5
  144. data/lib/hooksniff.rb +0 -19
@@ -1,213 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "models"
4
-
5
- module HookSniff
6
- class Client
7
- attr_reader :endpoints, :webhooks
8
-
9
- def initialize(api_key:, base_url: nil, timeout: nil)
10
- @api_key = api_key
11
- @base_url = (base_url || DEFAULT_BASE_URL).chomp("/")
12
- @timeout = timeout || DEFAULT_TIMEOUT
13
- @endpoints = EndpointsResource.new(self)
14
- @webhooks = WebhooksResource.new(self)
15
- end
16
-
17
- # Get platform statistics
18
- def stats
19
- resp = request(:get, "/stats")
20
- Models::Stats.new(resp)
21
- end
22
-
23
- # @api internal
24
- def request(method, path, body: nil)
25
- uri = URI("#{@base_url}#{path}")
26
- http = Net::HTTP.new(uri.host, uri.port)
27
- http.use_ssl = (uri.scheme == "https")
28
- http.open_timeout = @timeout
29
- http.read_timeout = @timeout
30
-
31
- case method
32
- when :get
33
- req = Net::HTTP::Get.new(uri)
34
- when :post
35
- req = Net::HTTP::Post.new(uri)
36
- when :delete
37
- req = Net::HTTP::Delete.new(uri)
38
- else
39
- raise ArgumentError, "Unsupported HTTP method: #{method}"
40
- end
41
-
42
- req["Authorization"] = "Bearer #{@api_key}"
43
- req["Content-Type"] = "application/json"
44
- req["User-Agent"] = "hooksniff-ruby/#{VERSION}"
45
-
46
- req.body = JSON.generate(body) if body
47
-
48
- response = http.request(req)
49
-
50
- case response.code.to_i
51
- when 200..299
52
- content_type = response["content-type"] || ""
53
- if content_type.include?("text/csv")
54
- response.body
55
- else
56
- JSON.parse(response.body) rescue response.body
57
- end
58
- when 400
59
- raise ValidationError, parse_error_message(response)
60
- when 401
61
- raise AuthenticationError, parse_error_message(response)
62
- when 404
63
- raise NotFoundError, parse_error_message(response)
64
- when 413
65
- raise PayloadTooLargeError, parse_error_message(response)
66
- when 429
67
- raise RateLimitError, parse_error_message(response)
68
- else
69
- raise Error, "HTTP #{response.code}: #{parse_error_message(response)}"
70
- end
71
- end
72
-
73
- private
74
-
75
- def parse_error_message(response)
76
- body = JSON.parse(response.body)
77
- body.dig("error", "message") || "HTTP #{response.code}"
78
- rescue
79
- "HTTP #{response.code}"
80
- end
81
- end
82
-
83
- class EndpointsResource
84
- def initialize(client)
85
- @client = client
86
- end
87
-
88
- def create(url:, description: nil, retry_policy: nil)
89
- body = { url: url }
90
- body[:description] = description if description
91
- if retry_policy
92
- body[:retry_policy] = {
93
- max_attempts: retry_policy[:max_attempts],
94
- backoff: retry_policy[:backoff],
95
- initial_delay_secs: retry_policy[:initial_delay_secs],
96
- max_delay_secs: retry_policy[:max_delay_secs]
97
- }.compact
98
- end
99
-
100
- resp = @client.request(:post, "/endpoints", body: body)
101
- Models::Endpoint.new(resp)
102
- end
103
-
104
- def get(endpoint_id)
105
- resp = @client.request(:get, "/endpoints/#{endpoint_id}")
106
- Models::Endpoint.new(resp)
107
- end
108
-
109
- def list(page: 1, per_page: 20)
110
- params = { page: page.to_s, per_page: per_page.to_s }
111
- query = URI.encode_www_form(params)
112
- resp = @client.request(:get, "/endpoints?#{query}")
113
- {
114
- endpoints: (resp["endpoints"] || resp).map { |ep| Models::Endpoint.new(ep) },
115
- total: resp["total"] || 0,
116
- page: resp["page"] || page,
117
- per_page: resp["per_page"] || per_page
118
- }
119
- end
120
-
121
- def delete(endpoint_id)
122
- resp = @client.request(:delete, "/endpoints/#{endpoint_id}")
123
- resp["deleted"] != false
124
- end
125
-
126
- def rotate_secret(endpoint_id)
127
- @client.request(:post, "/endpoints/#{endpoint_id}/rotate-secret")
128
- end
129
-
130
- private
131
- end
132
-
133
- class WebhooksResource
134
- def initialize(client)
135
- @client = client
136
- end
137
-
138
- # Send a webhook
139
- def send(endpoint_id:, event: nil, data:)
140
- body = { endpoint_id: endpoint_id, data: data }
141
- body[:event] = event if event
142
- resp = @client.request(:post, "/webhooks", body: body)
143
- Models::Delivery.new(resp)
144
- end
145
-
146
- # Get a delivery by ID
147
- def get(delivery_id)
148
- resp = @client.request(:get, "/webhooks/#{delivery_id}")
149
- Models::Delivery.new(resp)
150
- end
151
-
152
- # List deliveries with optional filters
153
- def list(status: nil, page: 1, per_page: 20)
154
- params = { page: page.to_s, per_page: per_page.to_s }
155
- params[:status] = status if status
156
- query = URI.encode_www_form(params)
157
- resp = @client.request(:get, "/webhooks?#{query}")
158
- Models::DeliveryList.new(resp)
159
- end
160
-
161
- # Replay a delivery
162
- def replay(delivery_id)
163
- resp = @client.request(:post, "/webhooks/#{delivery_id}/replay")
164
- Models::Delivery.new(resp)
165
- end
166
-
167
- # Send multiple webhooks in a batch
168
- def batch(webhooks)
169
- body = {
170
- webhooks: webhooks.map do |w|
171
- item = { endpoint_id: w[:endpoint_id], data: w[:data] }
172
- item[:event] = w[:event] if w[:event]
173
- item
174
- end
175
- }
176
- resp = @client.request(:post, "/webhooks/batch", body: body)
177
- Models::BatchResult.new(resp)
178
- end
179
-
180
- # Get delivery attempts
181
- def attempts(delivery_id)
182
- resp = @client.request(:get, "/webhooks/#{delivery_id}/attempts")
183
- resp.map { |a| Models::DeliveryAttempt.new(a) }
184
- end
185
-
186
- # Export deliveries
187
- def export(format: nil, status: nil, date_from: nil, date_to: nil)
188
- params = {}
189
- params[:format] = format if format
190
- params[:status] = status if status
191
- params[:date_from] = date_from if date_from
192
- params[:date_to] = date_to if date_to
193
- query = URI.encode_www_form(params)
194
- query = "?#{query}" unless query.empty?
195
-
196
- resp = @client.request(:get, "/webhooks/export#{query}")
197
- return resp if format == "csv"
198
-
199
- resp.map { |d| Models::Delivery.new(d) }
200
- end
201
-
202
- # Search deliveries with filters
203
- def search(query: nil, event: nil, status: nil, endpoint_id: nil, page: 1, per_page: 20)
204
- params = { page: page.to_s, per_page: per_page.to_s }
205
- params[:q] = query if query
206
- params[:event] = event if event
207
- params[:status] = status if status
208
- params[:endpoint_id] = endpoint_id if endpoint_id
209
- query_str = URI.encode_www_form(params)
210
- @client.request(:get, "/search?#{query_str}")
211
- end
212
- end
213
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HookSniff
4
- class Error < StandardError
5
- attr_reader :status_code, :error_code
6
-
7
- def initialize(message, status_code: nil, error_code: nil)
8
- super(message)
9
- @status_code = status_code
10
- @error_code = error_code
11
- end
12
- end
13
-
14
- class AuthenticationError < Error
15
- def initialize(message = "Unauthorized: invalid or missing API key")
16
- super(message, status_code: 401, error_code: "UNAUTHORIZED")
17
- end
18
- end
19
-
20
- class NotFoundError < Error
21
- def initialize(message = "Resource not found")
22
- super(message, status_code: 404, error_code: "NOT_FOUND")
23
- end
24
- end
25
-
26
- class RateLimitError < Error
27
- def initialize(message = "Rate limit exceeded")
28
- super(message, status_code: 429, error_code: "RATE_LIMIT_EXCEEDED")
29
- end
30
- end
31
-
32
- class ValidationError < Error
33
- def initialize(message = "Bad request")
34
- super(message, status_code: 400, error_code: "BAD_REQUEST")
35
- end
36
- end
37
-
38
- class PayloadTooLargeError < Error
39
- def initialize(message = "Payload too large")
40
- super(message, status_code: 413, error_code: "PAYLOAD_TOO_LARGE")
41
- end
42
- end
43
- end
@@ -1,136 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HookSniff
4
- module Models
5
- class Endpoint
6
- attr_reader :id, :url, :description, :is_active, :retry_policy, :created_at
7
-
8
- def initialize(data)
9
- @id = data["id"]
10
- @url = data["url"]
11
- @description = data["description"]
12
- @is_active = data["is_active"]
13
- @retry_policy = data["retry_policy"] ? RetryPolicy.new(data["retry_policy"]) : nil
14
- @created_at = data["created_at"]
15
- end
16
-
17
- def to_h
18
- {
19
- id: @id,
20
- url: @url,
21
- description: @description,
22
- is_active: @is_active,
23
- retry_policy: @retry_policy&.to_h,
24
- created_at: @created_at
25
- }
26
- end
27
- end
28
-
29
- class RetryPolicy
30
- attr_reader :max_attempts, :backoff, :initial_delay_secs, :max_delay_secs
31
-
32
- def initialize(data)
33
- @max_attempts = data["max_attempts"]
34
- @backoff = data["backoff"]
35
- @initial_delay_secs = data["initial_delay_secs"]
36
- @max_delay_secs = data["max_delay_secs"]
37
- end
38
-
39
- def to_h
40
- {
41
- max_attempts: @max_attempts,
42
- backoff: @backoff,
43
- initial_delay_secs: @initial_delay_secs,
44
- max_delay_secs: @max_delay_secs
45
- }.compact
46
- end
47
- end
48
-
49
- class Delivery
50
- attr_reader :id, :endpoint_id, :event, :status, :attempt_count, :response_status, :replay_count, :created_at
51
-
52
- def initialize(data)
53
- @id = data["id"]
54
- @endpoint_id = data["endpoint_id"]
55
- @event = data["event"]
56
- @status = data["status"]
57
- @attempt_count = data["attempt_count"] || 0
58
- @response_status = data["response_status"]
59
- @replay_count = data["replay_count"] || 0
60
- @created_at = data["created_at"]
61
- end
62
-
63
- def to_h
64
- {
65
- id: @id,
66
- endpoint_id: @endpoint_id,
67
- event: @event,
68
- status: @status,
69
- attempt_count: @attempt_count,
70
- response_status: @response_status,
71
- replay_count: @replay_count,
72
- created_at: @created_at
73
- }
74
- end
75
- end
76
-
77
- class DeliveryAttempt
78
- attr_reader :id, :attempt_number, :status_code, :response_body, :duration_ms, :error_message, :created_at
79
-
80
- def initialize(data)
81
- @id = data["id"]
82
- @attempt_number = data["attempt_number"]
83
- @status_code = data["status_code"]
84
- @response_body = data["response_body"]
85
- @duration_ms = data["duration_ms"]
86
- @error_message = data["error_message"]
87
- @created_at = data["created_at"]
88
- end
89
-
90
- def to_h
91
- {
92
- id: @id,
93
- attempt_number: @attempt_number,
94
- status_code: @status_code,
95
- response_body: @response_body,
96
- duration_ms: @duration_ms,
97
- error_message: @error_message,
98
- created_at: @created_at
99
- }
100
- end
101
- end
102
-
103
- class DeliveryList
104
- attr_reader :deliveries, :total, :page, :per_page
105
-
106
- def initialize(data)
107
- @deliveries = (data["deliveries"] || []).map { |d| Delivery.new(d) }
108
- @total = data["total"]
109
- @page = data["page"]
110
- @per_page = data["per_page"]
111
- end
112
- end
113
-
114
- class BatchResult
115
- attr_reader :deliveries, :errors
116
-
117
- def initialize(data)
118
- @deliveries = (data["deliveries"] || []).map { |d| Delivery.new(d) }
119
- @errors = data["errors"] || []
120
- end
121
- end
122
-
123
- class Stats
124
- attr_reader :total_deliveries, :delivered, :failed, :pending, :success_rate, :endpoints_count
125
-
126
- def initialize(data)
127
- @total_deliveries = data["total_deliveries"]
128
- @delivered = data["delivered"]
129
- @failed = data["failed"]
130
- @pending = data["pending"]
131
- @success_rate = data["success_rate"]
132
- @endpoints_count = data["endpoints_count"]
133
- end
134
- end
135
- end
136
- end
@@ -1,134 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HookSniff
4
- # Verify a webhook signature using HMAC-SHA256.
5
- #
6
- # @param payload [String] The raw request body
7
- # @param signature [String] The signature from the X-Hookrelay-Signature header
8
- # @param secret [String] The endpoint's signing secret (starts with "whsec_")
9
- # @return [Boolean] true if the signature is valid
10
- def self.verify_signature(payload, signature, secret)
11
- return false if payload.nil? || payload.empty?
12
- return false if signature.nil? || signature.empty?
13
- return false if secret.nil? || secret.empty?
14
-
15
- expected_hex = signature.start_with?("sha256=") ? signature[7..] : signature
16
-
17
- computed = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
18
-
19
- # Constant-time comparison to prevent timing attacks
20
- secure_compare(computed, expected_hex)
21
- rescue
22
- false
23
- end
24
-
25
- # Verify a webhook signature from an incoming request (Standard Webhooks + Svix compatible).
26
- #
27
- # Supports both Standard Webheaders headers (webhook-id, webhook-signature, webhook-timestamp)
28
- # and Svix headers (svix-id, svix-signature, svix-timestamp) as fallback.
29
- #
30
- # @param payload [String] The raw request body
31
- # @param headers [Hash] The request headers (symbol or string keys)
32
- # @param secret [String] The endpoint's signing secret
33
- # @param tolerance_secs [Integer] Max age in seconds (default: 300)
34
- # @return [Hash] { valid: bool, payload: parsed_data, error: string }
35
- def self.verify_webhook_from_headers(payload:, headers:, secret:, tolerance_secs: 300)
36
- # Normalize header keys to lowercase strings
37
- normalized = headers.transform_keys { |k| k.to_s.downcase }
38
-
39
- msg_id = normalized["webhook-id"]
40
- timestamp = normalized["webhook-timestamp"]
41
- signature_header = normalized["webhook-signature"]
42
-
43
- # Fallback to Svix headers
44
- unless msg_id && timestamp && signature_header
45
- msg_id ||= normalized["svix-id"]
46
- timestamp ||= normalized["svix-timestamp"]
47
- signature_header ||= normalized["svix-signature"]
48
- end
49
-
50
- verify_webhook(
51
- payload: payload,
52
- msg_id: msg_id,
53
- timestamp: timestamp,
54
- signature_header: signature_header,
55
- secret: secret,
56
- tolerance_secs: tolerance_secs,
57
- )
58
- end
59
-
60
- # Verify a webhook signature from an incoming request (Standard Webhooks compatible).
61
- #
62
- # @param payload [String] The raw request body
63
- # @param msg_id [String] The webhook-id header
64
- # @param timestamp [String] The webhook-timestamp header
65
- # @param signature_header [String] The webhook-signature header
66
- # @param secret [String] The endpoint's signing secret
67
- # @param tolerance_secs [Integer] Max age in seconds (default: 300)
68
- # @return [Hash] { valid: bool, payload: parsed_data, error: string }
69
- def self.verify_webhook(payload:, msg_id:, timestamp:, signature_header:, secret:, tolerance_secs: 300)
70
- return { valid: false, error: "Missing webhook-id header" } if msg_id.nil? || msg_id.empty?
71
- return { valid: false, error: "Missing webhook-timestamp header" } if timestamp.nil? || timestamp.empty?
72
- return { valid: false, error: "Missing webhook-signature header" } if signature_header.nil? || signature_header.empty?
73
- return { valid: false, error: "Missing request body" } if payload.nil? || payload.empty?
74
-
75
- ts = timestamp.to_i
76
- return { valid: false, error: "Invalid webhook timestamp" } if ts == 0
77
-
78
- now = Time.now.to_i
79
-
80
- if now - ts > tolerance_secs
81
- return { valid: false, error: "Message timestamp too old" }
82
- end
83
- if ts > now + tolerance_secs
84
- return { valid: false, error: "Message timestamp too new" }
85
- end
86
-
87
- # Compute expected signature
88
- signed_content = "#{msg_id}.#{timestamp}.#{payload}"
89
- secret_bytes = decode_secret(secret)
90
-
91
- expected_sig = Base64.strict_encode64(
92
- OpenSSL::HMAC.digest("SHA256", secret_bytes, signed_content)
93
- )
94
- expected_full = "v1,#{expected_sig}"
95
-
96
- # Check each signature in the header (space-separated)
97
- signatures = signature_header.split(" ")
98
- verified = signatures.any? do |sig|
99
- sig_stripped = sig.strip
100
- next unless sig_stripped.start_with?("v1,")
101
- secure_compare(sig_stripped, expected_full)
102
- end
103
-
104
- unless verified
105
- return { valid: false, error: "Invalid webhook signature" }
106
- end
107
-
108
- # Parse the payload
109
- begin
110
- parsed = JSON.parse(payload)
111
- { valid: true, payload: parsed }
112
- rescue JSON::ParserError
113
- { valid: true, payload: payload }
114
- end
115
- end
116
-
117
- private_class_method def self.decode_secret(secret)
118
- stripped = secret.start_with?("whsec_") ? secret[6..] : secret
119
- # Add padding in case secret is unpadded base64
120
- Base64.strict_decode64(stripped + "==")
121
- rescue ArgumentError
122
- secret.bytes.pack("C*")
123
- end
124
-
125
- # Constant-time string comparison
126
- def self.secure_compare(a, b)
127
- return false if a.nil? || b.nil?
128
- return false if a.bytesize != b.bytesize
129
-
130
- result = 0
131
- a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
132
- result == 0
133
- end
134
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HookSniff
4
- VERSION = "0.1.0"
5
- end
data/lib/hooksniff.rb DELETED
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "net/http"
5
- require "uri"
6
- require "openssl"
7
-
8
- require_relative "hooksniff/version"
9
- require_relative "hooksniff/errors"
10
- require_relative "hooksniff/client"
11
- require_relative "hooksniff/verification"
12
-
13
- module HookSniff
14
- # Default API base URL
15
- DEFAULT_BASE_URL = "https://hooksniff-api-1046140057667.europe-west1.run.app/v1"
16
-
17
- # Default request timeout in seconds
18
- DEFAULT_TIMEOUT = 30
19
- end