hookbridge 1.0.0 → 1.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/README.md +74 -201
- data/lib/hookbridge/client.rb +364 -179
- data/lib/hookbridge/types.rb +257 -223
- data/lib/hookbridge/version.rb +1 -1
- metadata +2 -2
data/lib/hookbridge/client.rb
CHANGED
|
@@ -5,93 +5,64 @@ require "faraday/retry"
|
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
7
|
module HookBridge
|
|
8
|
-
# Main client for interacting with the HookBridge API
|
|
9
8
|
class Client
|
|
10
9
|
DEFAULT_BASE_URL = "https://api.hookbridge.io"
|
|
10
|
+
DEFAULT_SEND_URL = "https://send.hookbridge.io"
|
|
11
11
|
DEFAULT_TIMEOUT = 30
|
|
12
12
|
DEFAULT_RETRIES = 3
|
|
13
13
|
|
|
14
|
-
attr_reader :api_key, :base_url, :timeout, :retries
|
|
14
|
+
attr_reader :api_key, :base_url, :send_url, :timeout, :retries
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
# @param api_key [String] Your HookBridge API key (starts with hb_live_ or hb_test_)
|
|
19
|
-
# @param base_url [String] API base URL (defaults to https://api.hookbridge.io)
|
|
20
|
-
# @param timeout [Integer] Request timeout in seconds (defaults to 30)
|
|
21
|
-
# @param retries [Integer] Number of retries for failed requests (defaults to 3)
|
|
22
|
-
def initialize(api_key:, base_url: nil, timeout: DEFAULT_TIMEOUT, retries: DEFAULT_RETRIES)
|
|
16
|
+
def initialize(api_key:, base_url: nil, send_url: nil, timeout: DEFAULT_TIMEOUT, retries: DEFAULT_RETRIES)
|
|
23
17
|
raise ValidationError, "API key is required" if api_key.nil? || api_key.empty?
|
|
24
18
|
|
|
25
19
|
@api_key = api_key
|
|
26
20
|
@base_url = (base_url || ENV.fetch("HOOKBRIDGE_BASE_URL", DEFAULT_BASE_URL)).chomp("/")
|
|
21
|
+
@send_url = (send_url || ENV.fetch("HOOKBRIDGE_SEND_URL", DEFAULT_SEND_URL)).chomp("/")
|
|
27
22
|
@timeout = timeout
|
|
28
23
|
@retries = retries
|
|
29
|
-
@connection = build_connection
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Send a webhook for guaranteed delivery
|
|
33
|
-
#
|
|
34
|
-
# @param endpoint [String] The registered endpoint identifier
|
|
35
|
-
# @param payload [Hash, String] The webhook payload
|
|
36
|
-
# @param idempotency_key [String, nil] Optional idempotency key to prevent duplicates
|
|
37
|
-
# @param content_type [String] Content type of the payload (defaults to application/json)
|
|
38
|
-
# @return [SendResponse] The send response with message_id and status
|
|
39
|
-
def send(endpoint:, payload:, idempotency_key: nil, content_type: "application/json")
|
|
40
|
-
body = {
|
|
41
|
-
endpoint: endpoint,
|
|
42
|
-
payload: payload,
|
|
43
|
-
content_type: content_type
|
|
44
|
-
}
|
|
45
|
-
body[:idempotency_key] = idempotency_key if idempotency_key
|
|
24
|
+
@connection = build_connection(@base_url)
|
|
25
|
+
@send_connection = build_connection(@send_url)
|
|
26
|
+
end
|
|
46
27
|
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
def send(endpoint_id:, payload:, headers: nil, idempotency_key: nil)
|
|
29
|
+
body = { endpoint_id: endpoint_id, payload: payload }
|
|
30
|
+
body[:headers] = headers if headers
|
|
31
|
+
body[:idempotency_key] = idempotency_key if idempotency_key
|
|
32
|
+
SendResponse.new(extract_data(request(:post, "/v1/webhooks/send", body, use_send_connection: true)))
|
|
49
33
|
end
|
|
50
34
|
|
|
51
|
-
# Get detailed status for a specific message
|
|
52
|
-
#
|
|
53
|
-
# @param message_id [String] The message ID (UUIDv7)
|
|
54
|
-
# @return [Message] The message details
|
|
55
35
|
def get_message(message_id)
|
|
56
|
-
|
|
57
|
-
Message.new(data)
|
|
36
|
+
Message.new(extract_data(request(:get, "/v1/messages/#{message_id}")))
|
|
58
37
|
end
|
|
59
38
|
|
|
60
|
-
# Manually replay a failed message
|
|
61
|
-
#
|
|
62
|
-
# @param message_id [String] The message ID to replay
|
|
63
|
-
# @return [ReplayResponse] The replay response
|
|
64
39
|
def replay(message_id)
|
|
65
|
-
|
|
66
|
-
ReplayResponse.new(data)
|
|
40
|
+
ReplayResponse.new(extract_data(request(:post, "/v1/messages/#{message_id}/replay")))
|
|
67
41
|
end
|
|
68
42
|
|
|
69
|
-
# Cancel a pending retry
|
|
70
|
-
#
|
|
71
|
-
# @param message_id [String] The message ID
|
|
72
|
-
# @return [Message] The updated message
|
|
73
43
|
def cancel_retry(message_id)
|
|
74
|
-
|
|
75
|
-
|
|
44
|
+
request(:post, "/v1/messages/#{message_id}/cancel")
|
|
45
|
+
nil
|
|
76
46
|
end
|
|
77
47
|
|
|
78
|
-
# Trigger immediate retry for a pending message
|
|
79
|
-
#
|
|
80
|
-
# @param message_id [String] The message ID
|
|
81
|
-
# @return [Message] The updated message
|
|
82
48
|
def retry_now(message_id)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
49
|
+
request(:post, "/v1/messages/#{message_id}/retry-now")
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def replay_all_messages(status:, endpoint_id: nil, limit: nil)
|
|
54
|
+
params = { status: status }
|
|
55
|
+
params[:endpoint_id] = endpoint_id if endpoint_id
|
|
56
|
+
params[:limit] = limit if limit
|
|
57
|
+
ReplayAllMessagesResponse.new(extract_data(request(:post, "/v1/messages/replay-all", nil, params)))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def replay_batch_messages(message_ids)
|
|
61
|
+
ReplayBatchMessagesResponse.new(
|
|
62
|
+
extract_data(request(:post, "/v1/messages/replay-batch", { message_ids: message_ids }))
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
95
66
|
def get_logs(status: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
|
|
96
67
|
params = {}
|
|
97
68
|
params[:status] = status if status
|
|
@@ -99,200 +70,414 @@ module HookBridge
|
|
|
99
70
|
params[:end_time] = format_time(end_time) if end_time
|
|
100
71
|
params[:limit] = limit if limit
|
|
101
72
|
params[:cursor] = cursor if cursor
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
73
|
+
response = request(:get, "/v1/logs", nil, params)
|
|
74
|
+
LogsResponse.new(
|
|
75
|
+
"data" => extract_data(response),
|
|
76
|
+
"has_more" => extract_meta(response)["has_more"] || response["has_more"],
|
|
77
|
+
"next_cursor" => extract_meta(response)["next_cursor"] || response["next_cursor"]
|
|
78
|
+
)
|
|
105
79
|
end
|
|
106
80
|
|
|
107
|
-
# Retrieve aggregated delivery metrics
|
|
108
|
-
#
|
|
109
|
-
# @param window [String] Time window: "1h", "24h", "7d", or "30d"
|
|
110
|
-
# @return [Metrics] The delivery metrics
|
|
111
81
|
def get_metrics(window: MetricsWindow::TWENTY_FOUR_HOURS)
|
|
112
|
-
|
|
113
|
-
|
|
82
|
+
Metrics.new(extract_data(request(:get, "/v1/metrics", nil, { window: window })))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_timeseries_metrics(window: MetricsWindow::TWENTY_FOUR_HOURS, endpoint_id: nil)
|
|
86
|
+
params = { window: window }
|
|
87
|
+
params[:endpoint_id] = endpoint_id if endpoint_id
|
|
88
|
+
TimeSeriesMetrics.new(extract_data(request(:get, "/v1/metrics/timeseries", nil, params)))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def get_subscription
|
|
92
|
+
Subscription.new(extract_data(request(:get, "/v1/billing/subscription")))
|
|
114
93
|
end
|
|
115
94
|
|
|
116
|
-
# List messages in the Dead Letter Queue
|
|
117
|
-
#
|
|
118
|
-
# @param limit [Integer, nil] Maximum number of results
|
|
119
|
-
# @param cursor [String, nil] Pagination cursor
|
|
120
|
-
# @return [DLQResponse] Paginated list of DLQ messages
|
|
121
95
|
def get_dlq_messages(limit: nil, cursor: nil)
|
|
122
96
|
params = {}
|
|
123
97
|
params[:limit] = limit if limit
|
|
124
98
|
params[:cursor] = cursor if cursor
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
response = request(:get, "/v1/dlq/messages", nil, params)
|
|
100
|
+
DLQResponse.new(
|
|
101
|
+
"data" => extract_data(response),
|
|
102
|
+
"has_more" => extract_data(response)["has_more"] || response["has_more"],
|
|
103
|
+
"next_cursor" => extract_data(response)["next_cursor"] || response["next_cursor"]
|
|
104
|
+
)
|
|
128
105
|
end
|
|
129
106
|
|
|
130
|
-
# Replay a message from the Dead Letter Queue
|
|
131
|
-
#
|
|
132
|
-
# @param message_id [String] The message ID to replay
|
|
133
|
-
# @return [ReplayResponse] The replay response
|
|
134
107
|
def replay_from_dlq(message_id)
|
|
135
|
-
|
|
136
|
-
ReplayResponse.new(data)
|
|
108
|
+
ReplayResponse.new(extract_data(request(:post, "/v1/dlq/replay/#{message_id}")))
|
|
137
109
|
end
|
|
138
110
|
|
|
139
|
-
# List all API keys for the project
|
|
140
|
-
#
|
|
141
|
-
# @return [APIKeysResponse] List of API keys
|
|
142
111
|
def list_api_keys
|
|
143
|
-
data
|
|
144
|
-
APIKeysResponse.new(data)
|
|
112
|
+
APIKeysResponse.new("data" => extract_data(request(:get, "/v1/api-keys")))
|
|
145
113
|
end
|
|
146
114
|
|
|
147
|
-
# Create a new API key
|
|
148
|
-
#
|
|
149
|
-
# @param name [String] Name for the API key
|
|
150
|
-
# @param mode [String] Key mode: "live" or "test"
|
|
151
|
-
# @return [APIKeyCreated] The created API key (includes full key, only shown once)
|
|
152
115
|
def create_api_key(label: nil, mode: APIKeyMode::LIVE)
|
|
153
116
|
body = { mode: mode }
|
|
154
117
|
body[:label] = label if label
|
|
155
|
-
|
|
156
|
-
APIKeyCreated.new(data)
|
|
118
|
+
APIKeyCreated.new(extract_data(request(:post, "/v1/api-keys", body)))
|
|
157
119
|
end
|
|
158
120
|
|
|
159
|
-
# Delete an API key
|
|
160
|
-
#
|
|
161
|
-
# @param key_id [String] The API key ID to delete
|
|
162
|
-
# @return [Boolean] true if successful
|
|
163
121
|
def delete_api_key(key_id)
|
|
164
122
|
request(:delete, "/v1/api-keys/#{key_id}")
|
|
165
123
|
true
|
|
166
124
|
end
|
|
167
125
|
|
|
126
|
+
def list_projects
|
|
127
|
+
extract_data(request(:get, "/v1/projects")).map { |project| Project.new(project) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def create_project(name:, rate_limit_default: nil)
|
|
131
|
+
body = { name: name }
|
|
132
|
+
body[:rate_limit_default] = rate_limit_default if rate_limit_default
|
|
133
|
+
Project.new(extract_data(request(:post, "/v1/projects", body)))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get_project(project_id)
|
|
137
|
+
Project.new(extract_data(request(:get, "/v1/projects/#{project_id}")))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def update_project(project_id, name: nil, rate_limit_default: nil)
|
|
141
|
+
body = {}
|
|
142
|
+
body[:name] = name if name
|
|
143
|
+
body[:rate_limit_default] = rate_limit_default if rate_limit_default
|
|
144
|
+
Project.new(extract_data(request(:put, "/v1/projects/#{project_id}", body)))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def delete_project(project_id)
|
|
148
|
+
request(:delete, "/v1/projects/#{project_id}")
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def create_endpoint(url:, description: nil, hmac_enabled: nil, rate_limit_rps: 0, burst: 0, headers: nil)
|
|
153
|
+
body = { url: url }
|
|
154
|
+
body[:description] = description if description
|
|
155
|
+
body[:hmac_enabled] = hmac_enabled unless hmac_enabled.nil?
|
|
156
|
+
body[:rate_limit_rps] = rate_limit_rps if rate_limit_rps.positive?
|
|
157
|
+
body[:burst] = burst if burst.positive?
|
|
158
|
+
body[:headers] = headers if headers
|
|
159
|
+
CreateEndpointResponse.new(extract_data(request(:post, "/v1/endpoints", body)))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def get_endpoint(endpoint_id)
|
|
163
|
+
Endpoint.new(extract_data(request(:get, "/v1/endpoints/#{endpoint_id}")))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def list_endpoints(limit: nil, cursor: nil)
|
|
167
|
+
params = {}
|
|
168
|
+
params[:limit] = limit if limit
|
|
169
|
+
params[:cursor] = cursor if cursor
|
|
170
|
+
response = request(:get, "/v1/endpoints", nil, params)
|
|
171
|
+
ListEndpointsResponse.new(
|
|
172
|
+
"data" => extract_data(response),
|
|
173
|
+
"has_more" => extract_meta(response)["has_more"] || response["has_more"],
|
|
174
|
+
"next_cursor" => extract_meta(response)["next_cursor"] || response["next_cursor"]
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def update_endpoint(endpoint_id, url: nil, description: nil, hmac_enabled: nil, rate_limit_rps: nil, burst: nil, headers: nil)
|
|
179
|
+
body = {}
|
|
180
|
+
body[:url] = url if url
|
|
181
|
+
body[:description] = description if description
|
|
182
|
+
body[:hmac_enabled] = hmac_enabled unless hmac_enabled.nil?
|
|
183
|
+
body[:rate_limit_rps] = rate_limit_rps if rate_limit_rps
|
|
184
|
+
body[:burst] = burst if burst
|
|
185
|
+
body[:headers] = headers if headers
|
|
186
|
+
request(:patch, "/v1/endpoints/#{endpoint_id}", body)
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def delete_endpoint(endpoint_id)
|
|
191
|
+
request(:delete, "/v1/endpoints/#{endpoint_id}")
|
|
192
|
+
true
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def create_endpoint_signing_key(endpoint_id)
|
|
196
|
+
RotateSecretResponse.new(extract_data(request(:post, "/v1/endpoints/#{endpoint_id}/signing-keys")))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def list_endpoint_signing_keys(endpoint_id)
|
|
200
|
+
extract_data(request(:get, "/v1/endpoints/#{endpoint_id}/signing-keys")).map do |entry|
|
|
201
|
+
SigningKey.new(entry)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def delete_endpoint_signing_key(endpoint_id, key_id)
|
|
206
|
+
request(:delete, "/v1/endpoints/#{endpoint_id}/signing-keys/#{key_id}")
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def rotate_endpoint_secret(endpoint_id)
|
|
211
|
+
create_endpoint_signing_key(endpoint_id)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def create_checkout(plan:, interval:)
|
|
215
|
+
CheckoutSession.new(extract_data(request(:post, "/v1/billing/checkout", { plan: plan, interval: interval })))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def create_portal(return_url: nil)
|
|
219
|
+
body = {}
|
|
220
|
+
body[:return_url] = return_url if return_url
|
|
221
|
+
PortalSession.new(extract_data(request(:post, "/v1/billing/portal", body)))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def get_usage_history(limit: 12, offset: 0)
|
|
225
|
+
response = request(:get, "/v1/billing/usage-history", nil, { limit: limit, offset: offset })
|
|
226
|
+
UsageHistoryResponse.new(extract_data(response), extract_meta(response))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def get_invoices(limit: 12, starting_after: nil)
|
|
230
|
+
params = { limit: limit }
|
|
231
|
+
params[:starting_after] = starting_after if starting_after
|
|
232
|
+
response = request(:get, "/v1/billing/invoices", nil, params)
|
|
233
|
+
InvoicesResponse.new(extract_data(response), extract_meta(response))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def create_inbound_endpoint(url:, name: nil, description: nil, verify_static_token: nil, token_header_name: nil, token_query_param: nil, token_value: nil, verify_hmac: nil, hmac_header_name: nil, hmac_secret: nil, timestamp_header_name: nil, timestamp_ttl_seconds: nil, verify_ip_allowlist: nil, allowed_cidrs: nil, idempotency_header_names: nil, ingest_response_code: nil, signing_enabled: nil)
|
|
237
|
+
body = { url: url }
|
|
238
|
+
{
|
|
239
|
+
name: name,
|
|
240
|
+
description: description,
|
|
241
|
+
verify_static_token: verify_static_token,
|
|
242
|
+
token_header_name: token_header_name,
|
|
243
|
+
token_query_param: token_query_param,
|
|
244
|
+
token_value: token_value,
|
|
245
|
+
verify_hmac: verify_hmac,
|
|
246
|
+
hmac_header_name: hmac_header_name,
|
|
247
|
+
hmac_secret: hmac_secret,
|
|
248
|
+
timestamp_header_name: timestamp_header_name,
|
|
249
|
+
timestamp_ttl_seconds: timestamp_ttl_seconds,
|
|
250
|
+
verify_ip_allowlist: verify_ip_allowlist,
|
|
251
|
+
allowed_cidrs: allowed_cidrs,
|
|
252
|
+
idempotency_header_names: idempotency_header_names,
|
|
253
|
+
ingest_response_code: ingest_response_code,
|
|
254
|
+
signing_enabled: signing_enabled
|
|
255
|
+
}.each do |key, value|
|
|
256
|
+
body[key] = value unless value.nil?
|
|
257
|
+
end
|
|
258
|
+
CreateInboundEndpointResponse.new(extract_data(request(:post, "/v1/inbound-endpoints", body)))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def list_inbound_endpoints(limit: nil, cursor: nil)
|
|
262
|
+
params = {}
|
|
263
|
+
params[:limit] = limit if limit
|
|
264
|
+
params[:cursor] = cursor if cursor
|
|
265
|
+
response = request(:get, "/v1/inbound-endpoints", nil, params)
|
|
266
|
+
ListInboundEndpointsResponse.new(
|
|
267
|
+
"data" => extract_data(response),
|
|
268
|
+
"has_more" => extract_meta(response)["has_more"] || response["has_more"],
|
|
269
|
+
"next_cursor" => extract_meta(response)["next_cursor"] || response["next_cursor"]
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def get_inbound_endpoint(endpoint_id)
|
|
274
|
+
InboundEndpoint.new(extract_data(request(:get, "/v1/inbound-endpoints/#{endpoint_id}")))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def update_inbound_endpoint(endpoint_id, **attributes)
|
|
278
|
+
UpdateResult.new(extract_data(request(:patch, "/v1/inbound-endpoints/#{endpoint_id}", compact_hash(attributes))))
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def delete_inbound_endpoint(endpoint_id)
|
|
282
|
+
DeleteResult.new(extract_data(request(:delete, "/v1/inbound-endpoints/#{endpoint_id}")))
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def pause_inbound_endpoint(endpoint_id)
|
|
286
|
+
PauseState.new(extract_data(request(:post, "/v1/inbound-endpoints/#{endpoint_id}/pause")))
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def resume_inbound_endpoint(endpoint_id)
|
|
290
|
+
PauseState.new(extract_data(request(:post, "/v1/inbound-endpoints/#{endpoint_id}/resume")))
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def replay_inbound_message(message_id)
|
|
294
|
+
ReplayResponse.new(extract_data(request(:post, "/v1/inbound-messages/#{message_id}/replay")))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def replay_all_inbound_messages(status:, inbound_endpoint_id: nil, limit: nil)
|
|
298
|
+
params = { status: status }
|
|
299
|
+
params[:inbound_endpoint_id] = inbound_endpoint_id if inbound_endpoint_id
|
|
300
|
+
params[:limit] = limit if limit
|
|
301
|
+
ReplayAllMessagesResponse.new(extract_data(request(:post, "/v1/inbound-messages/replay-all", nil, params)))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def replay_batch_inbound_messages(message_ids)
|
|
305
|
+
ReplayBatchMessagesResponse.new(
|
|
306
|
+
extract_data(request(:post, "/v1/inbound-messages/replay-batch", { message_ids: message_ids }))
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def get_inbound_logs(status: nil, inbound_endpoint_id: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
|
|
311
|
+
params = {}
|
|
312
|
+
params[:status] = status if status
|
|
313
|
+
params[:inbound_endpoint_id] = inbound_endpoint_id if inbound_endpoint_id
|
|
314
|
+
params[:start_time] = format_time(start_time) if start_time
|
|
315
|
+
params[:end_time] = format_time(end_time) if end_time
|
|
316
|
+
params[:limit] = limit if limit
|
|
317
|
+
params[:cursor] = cursor if cursor
|
|
318
|
+
response = request(:get, "/v1/inbound-logs", nil, params)
|
|
319
|
+
InboundLogsResponse.new(
|
|
320
|
+
"data" => extract_data(response),
|
|
321
|
+
"has_more" => extract_meta(response)["has_more"] || response["has_more"],
|
|
322
|
+
"next_cursor" => extract_meta(response)["next_cursor"] || response["next_cursor"]
|
|
323
|
+
)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def get_inbound_metrics(window: MetricsWindow::TWENTY_FOUR_HOURS, inbound_endpoint_id: nil)
|
|
327
|
+
params = { window: window }
|
|
328
|
+
params[:inbound_endpoint_id] = inbound_endpoint_id if inbound_endpoint_id
|
|
329
|
+
InboundMetrics.new(extract_data(request(:get, "/v1/inbound-metrics", nil, params)))
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def get_inbound_timeseries_metrics(window: MetricsWindow::TWENTY_FOUR_HOURS, inbound_endpoint_id: nil)
|
|
333
|
+
params = { window: window }
|
|
334
|
+
params[:inbound_endpoint_id] = inbound_endpoint_id if inbound_endpoint_id
|
|
335
|
+
TimeSeriesMetrics.new(extract_data(request(:get, "/v1/inbound-metrics/timeseries", nil, params)))
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def list_inbound_rejections(inbound_endpoint_id: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
|
|
339
|
+
params = {}
|
|
340
|
+
params[:inbound_endpoint_id] = inbound_endpoint_id if inbound_endpoint_id
|
|
341
|
+
params[:start_time] = format_time(start_time) if start_time
|
|
342
|
+
params[:end_time] = format_time(end_time) if end_time
|
|
343
|
+
params[:limit] = limit if limit
|
|
344
|
+
params[:cursor] = cursor if cursor
|
|
345
|
+
response = request(:get, "/v1/inbound-rejections", nil, params)
|
|
346
|
+
InboundRejectionsResponse.new(
|
|
347
|
+
"data" => extract_data(response),
|
|
348
|
+
"has_more" => extract_meta(response)["has_more"] || response["has_more"],
|
|
349
|
+
"next_cursor" => extract_meta(response)["next_cursor"] || response["next_cursor"]
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def create_export(start_time:, end_time:, status: nil, endpoint_id: nil)
|
|
354
|
+
body = { start_time: format_time(start_time), end_time: format_time(end_time) }
|
|
355
|
+
body[:status] = status if status
|
|
356
|
+
body[:endpoint_id] = endpoint_id if endpoint_id
|
|
357
|
+
ExportRecord.new(extract_data(request(:post, "/v1/exports", body)))
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def list_exports
|
|
361
|
+
extract_data(request(:get, "/v1/exports")).map { |entry| ExportRecord.new(entry) }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def get_export(export_id)
|
|
365
|
+
ExportRecord.new(extract_data(request(:get, "/v1/exports/#{export_id}")))
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def download_export(export_id)
|
|
369
|
+
request(:get, "/v1/exports/#{export_id}/download", nil, nil, allow_redirect: true)
|
|
370
|
+
end
|
|
371
|
+
|
|
168
372
|
private
|
|
169
373
|
|
|
170
|
-
def build_connection
|
|
171
|
-
Faraday.new(url:
|
|
374
|
+
def build_connection(url)
|
|
375
|
+
Faraday.new(url: url) do |conn|
|
|
172
376
|
conn.request :retry,
|
|
173
377
|
max: @retries,
|
|
174
378
|
interval: 0.1,
|
|
175
379
|
interval_randomness: 0.5,
|
|
176
380
|
backoff_factor: 2,
|
|
177
381
|
retry_statuses: [500, 502, 503, 504],
|
|
178
|
-
methods: %i[get post put delete]
|
|
179
|
-
retry_block: lambda { |env, _options, retries, exception|
|
|
180
|
-
# Don't retry client errors
|
|
181
|
-
return false if env.status && env.status < 500
|
|
182
|
-
|
|
183
|
-
true
|
|
184
|
-
}
|
|
382
|
+
methods: %i[get post put patch delete]
|
|
185
383
|
|
|
186
384
|
conn.options.timeout = @timeout
|
|
187
385
|
conn.options.open_timeout = 10
|
|
188
|
-
|
|
189
386
|
conn.headers["Authorization"] = "Bearer #{@api_key}"
|
|
190
387
|
conn.headers["Content-Type"] = "application/json"
|
|
191
388
|
conn.headers["Accept"] = "application/json"
|
|
192
389
|
conn.headers["User-Agent"] = "hookbridge-ruby/#{VERSION}"
|
|
193
|
-
|
|
194
390
|
conn.adapter Faraday.default_adapter
|
|
195
391
|
end
|
|
196
392
|
end
|
|
197
393
|
|
|
198
|
-
def request(method, path, body = nil, params = nil)
|
|
199
|
-
|
|
394
|
+
def request(method, path, body = nil, params = nil, use_send_connection: false, allow_redirect: false)
|
|
395
|
+
conn = use_send_connection ? @send_connection : @connection
|
|
396
|
+
response = conn.run_request(method, path, body&.to_json, nil) do |req|
|
|
200
397
|
req.params.update(params) if params
|
|
201
398
|
end
|
|
202
399
|
|
|
203
|
-
handle_response(response)
|
|
400
|
+
handle_response(response, allow_redirect: allow_redirect)
|
|
204
401
|
rescue Faraday::TimeoutError => e
|
|
205
|
-
raise TimeoutError
|
|
402
|
+
raise TimeoutError, "Request timed out: #{e.message}"
|
|
206
403
|
rescue Faraday::ConnectionFailed => e
|
|
207
|
-
raise NetworkError
|
|
404
|
+
raise NetworkError, "Connection failed: #{e.message}"
|
|
208
405
|
rescue Faraday::Error => e
|
|
209
|
-
raise NetworkError
|
|
406
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
210
407
|
end
|
|
211
408
|
|
|
212
|
-
def handle_response(response)
|
|
213
|
-
|
|
409
|
+
def handle_response(response, allow_redirect: false)
|
|
410
|
+
return response.headers["location"] if allow_redirect && response.status.between?(300, 399)
|
|
411
|
+
return {} if response.status == 204
|
|
214
412
|
|
|
215
413
|
case response.status
|
|
216
414
|
when 200..299
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
# API wraps responses in { data: ..., meta: ... }
|
|
221
|
-
# Return data field if present, otherwise return parsed response
|
|
222
|
-
if parsed.is_a?(Hash) && parsed.key?("data")
|
|
223
|
-
# For paginated responses, include meta info
|
|
224
|
-
if parsed["meta"]&.key?("has_more") || parsed["meta"]&.key?("next_cursor")
|
|
225
|
-
{
|
|
226
|
-
"data" => parsed["data"],
|
|
227
|
-
"has_more" => parsed.dig("meta", "has_more"),
|
|
228
|
-
"next_cursor" => parsed.dig("meta", "next_cursor")
|
|
229
|
-
}
|
|
230
|
-
else
|
|
231
|
-
parsed["data"]
|
|
232
|
-
end
|
|
233
|
-
else
|
|
234
|
-
parsed
|
|
235
|
-
end
|
|
415
|
+
return {} if response.body.nil? || response.body.empty?
|
|
416
|
+
|
|
417
|
+
JSON.parse(response.body)
|
|
236
418
|
when 400
|
|
237
|
-
|
|
238
|
-
raise ValidationError.new(
|
|
419
|
+
error = parse_error(response.body)
|
|
420
|
+
raise ValidationError.new(error[:message], request_id: error[:request_id])
|
|
239
421
|
when 401
|
|
240
|
-
|
|
241
|
-
raise AuthenticationError.new(
|
|
422
|
+
error = parse_error(response.body)
|
|
423
|
+
raise AuthenticationError.new(error[:message], request_id: error[:request_id])
|
|
242
424
|
when 404
|
|
243
|
-
|
|
244
|
-
raise NotFoundError.new(
|
|
425
|
+
error = parse_error(response.body)
|
|
426
|
+
raise NotFoundError.new(error[:message], request_id: error[:request_id])
|
|
245
427
|
when 409
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
raise ReplayLimitError.new(error_data[:message], request_id: request_id)
|
|
250
|
-
else
|
|
251
|
-
raise IdempotencyError.new(error_data[:message], request_id: request_id)
|
|
428
|
+
error = parse_error(response.body)
|
|
429
|
+
if %w[IDEMPOTENCY_MISMATCH IDEMPOTENCY_CONFLICT].include?(error[:code])
|
|
430
|
+
raise IdempotencyError.new(error[:message], request_id: error[:request_id])
|
|
252
431
|
end
|
|
432
|
+
raise ReplayLimitError.new(error[:message], request_id: error[:request_id]) if error[:code] == "REPLAY_LIMIT_EXCEEDED"
|
|
433
|
+
|
|
434
|
+
raise Error.new(error[:message], code: error[:code], request_id: error[:request_id], status_code: 409)
|
|
253
435
|
when 429
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
436
|
+
error = parse_error(response.body)
|
|
437
|
+
raise ReplayLimitError.new(error[:message], request_id: error[:request_id]) if error[:code] == "REPLAY_LIMIT_EXCEEDED"
|
|
438
|
+
|
|
439
|
+
raise RateLimitError.new(error[:message], request_id: error[:request_id], retry_after: response.headers["retry-after"]&.to_i)
|
|
257
440
|
else
|
|
258
|
-
|
|
259
|
-
raise Error.new(
|
|
260
|
-
error_data[:message] || "HTTP #{response.status}",
|
|
261
|
-
code: error_data[:code],
|
|
262
|
-
request_id: request_id,
|
|
263
|
-
status_code: response.status
|
|
264
|
-
)
|
|
441
|
+
error = parse_error(response.body)
|
|
442
|
+
raise Error.new(error[:message], code: error[:code], request_id: error[:request_id], status_code: response.status)
|
|
265
443
|
end
|
|
266
444
|
end
|
|
267
445
|
|
|
268
446
|
def parse_error(body)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# API wraps errors in { error: { code: ..., message: ... }, meta: ... }
|
|
273
|
-
if data["error"].is_a?(Hash)
|
|
447
|
+
parsed = body && !body.empty? ? JSON.parse(body) : {}
|
|
448
|
+
error_data = parsed["error"]
|
|
449
|
+
if error_data.is_a?(Hash)
|
|
274
450
|
{
|
|
275
|
-
|
|
276
|
-
|
|
451
|
+
code: error_data["code"],
|
|
452
|
+
message: error_data["message"] || "Request failed",
|
|
453
|
+
request_id: parsed.dig("meta", "request_id")
|
|
277
454
|
}
|
|
278
455
|
else
|
|
279
456
|
{
|
|
280
|
-
|
|
281
|
-
|
|
457
|
+
code: parsed["code"],
|
|
458
|
+
message: error_data || parsed["message"] || "Request failed",
|
|
459
|
+
request_id: parsed.dig("meta", "request_id")
|
|
282
460
|
}
|
|
283
461
|
end
|
|
284
462
|
rescue JSON::ParserError
|
|
285
|
-
{ message:
|
|
463
|
+
{ code: nil, message: "Request failed", request_id: nil }
|
|
286
464
|
end
|
|
287
465
|
|
|
288
|
-
def
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
466
|
+
def extract_data(response)
|
|
467
|
+
response.is_a?(Hash) && response.key?("data") ? response["data"] : response
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def extract_meta(response)
|
|
471
|
+
response.is_a?(Hash) ? (response["meta"] || {}) : {}
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def format_time(value)
|
|
475
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def compact_hash(hash)
|
|
479
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
480
|
+
memo[key] = value unless value.nil?
|
|
296
481
|
end
|
|
297
482
|
end
|
|
298
483
|
end
|