hookd-client 1.0.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eade8cb3959a4a3792d6bf6c7db1cc9ea04fe6d1890ff895fe9400e823d720bf
4
- data.tar.gz: b49e156b95cac1a65897f3dfc8f85f55e96e8de9181885b9399357e1c8dbcc96
3
+ metadata.gz: 97edc24416cdc28af2b3e6506d598711705cfc948bbb24416256343ed07e70d9
4
+ data.tar.gz: '008e6b1dd48ea295b47551c1cf9182ea4beceacff62751db9ee0829157911378'
5
5
  SHA512:
6
- metadata.gz: 2fc01eb61437a52f4ed20cec5a9af0b4c408427da54fe87e8186163254f41105a6026970bb96bb32a886761fdb12bc329e99aa38a77f6d2f498573fbb0511b16
7
- data.tar.gz: 3f40e48b70f0ac245125382fc9ff5150f4fdfdae1439b8436bf22ce31e852bea9f36d033014f94d4dc61b66c91eea18d26dc8e9ccb13371462069d13977f0b37
6
+ metadata.gz: baefacc1c8e98030276e92c3ea4c7ab3e78bf8bef6465e0a99c4574ec221aae4070d3e5c10e353fb4b732532aa19031cc5697dab4e355fe984f730fcc46ecc97
7
+ data.tar.gz: c382ec4a08173ecead4a6334b095a112c3fce9eed944216b48634652dfc20b33d0a820d380920e6682c987f8ad718a0e906d015c94603ca87898da4c93ab83bd
data/README.md CHANGED
@@ -39,7 +39,7 @@ puts "HTTPS endpoint: #{hook.https}"
39
39
  # Make a request to the HTTP endpoint to simulate an interaction
40
40
  Typhoeus.get(hook.http)
41
41
 
42
- # Poll for interactions
42
+ # Poll for interactions (single hook)
43
43
  interactions = client.poll(hook.id)
44
44
  interactions.each do |interaction|
45
45
  if interaction.dns?
@@ -50,6 +50,60 @@ interactions.each do |interaction|
50
50
  end
51
51
  ```
52
52
 
53
+ ### Batch Polling Example
54
+
55
+ When working with multiple hooks, use `poll_batch` for better performance:
56
+
57
+ ```ruby
58
+ require 'hookd'
59
+ require 'typhoeus'
60
+
61
+ # Initialize the client
62
+ client = Hookd::Client.new(
63
+ server: "https://hookd.example.com",
64
+ token: ENV['HOOKD_TOKEN']
65
+ )
66
+
67
+ # Register multiple hooks
68
+ puts "Registering 5 hooks..."
69
+ hooks = client.register(count: 5)
70
+
71
+ hook_ids = hooks.map(&:id)
72
+ puts "Created hooks: #{hook_ids.join(', ')}"
73
+
74
+ # Simulate some interactions...
75
+ # (make DNS queries, HTTP requests, etc.)
76
+
77
+ # Simulate some interactions
78
+ puts "\nSimulating HTTP requests..."
79
+ hooks.each do |hook|
80
+ Typhoeus.get(hook.http)
81
+ puts " ✓ GET #{hook.http}"
82
+ end
83
+
84
+ # Batch poll all hooks at once (1 HTTP request instead of 5)
85
+ puts "Batch polling #{hook_ids.size} hooks..."
86
+ results = client.poll_batch(hook_ids)
87
+
88
+ # Display results
89
+ results.each do |hook_id, result|
90
+ if result[:error]
91
+ puts "❌ Hook #{hook_id}: #{result[:error]}"
92
+ else
93
+ interactions = result[:interactions]
94
+ puts "✅ Hook #{hook_id}: #{interactions.size} interaction(s)"
95
+
96
+ interactions.each do |interaction|
97
+ if interaction.dns?
98
+ puts " - DNS: #{interaction.data['qname']} (#{interaction.data['qtype']})"
99
+ elsif interaction.http?
100
+ puts " - HTTP: #{interaction.data['method']} #{interaction.data['path']}"
101
+ end
102
+ end
103
+ end
104
+ end
105
+ ```
106
+
53
107
  ### Configuration
54
108
 
55
109
  The client requires two configuration parameters:
@@ -94,7 +148,7 @@ Raises:
94
148
 
95
149
  ##### `#poll(hook_id)`
96
150
 
97
- Poll for interactions captured by a hook.
151
+ Poll for interactions captured by a single hook.
98
152
 
99
153
  ```ruby
100
154
  interactions = client.poll("abc123")
@@ -112,6 +166,55 @@ Raises:
112
166
  - `Hookd::ServerError` - Server error (5xx)
113
167
  - `Hookd::ConnectionError` - Connection failed
114
168
 
169
+ ##### `#poll_batch(hook_ids)`
170
+
171
+ **Batch poll** - Poll for interactions from multiple hooks in a single request.
172
+
173
+ ```ruby
174
+ # Register multiple hooks
175
+ hooks = client.register(count: 3)
176
+ hook_ids = hooks.map(&:id)
177
+
178
+ # Batch poll all hooks at once (1 HTTP request instead of 3)
179
+ results = client.poll_batch(hook_ids)
180
+ # => {
181
+ # "abc123" => { interactions: [...], error: nil },
182
+ # "def456" => { interactions: [...], error: nil },
183
+ # "ghi789" => { interactions: [], error: nil }
184
+ # }
185
+
186
+ # Process results
187
+ results.each do |hook_id, result|
188
+ if result[:error]
189
+ puts "Error for #{hook_id}: #{result[:error]}"
190
+ else
191
+ puts "Hook #{hook_id}: #{result[:interactions].size} interactions"
192
+ result[:interactions].each do |interaction|
193
+ puts " - #{interaction.type}: #{interaction.data}"
194
+ end
195
+ end
196
+ end
197
+ ```
198
+
199
+ Parameters:
200
+ - `hook_ids` (Array<String>) - Array of hook IDs to poll
201
+
202
+ Returns: Hash mapping hook IDs to results
203
+ - Each result contains:
204
+ - `interactions` (Array<Hookd::Interaction>) - Array of interactions
205
+ - `error` (String, nil) - Error message if hook not found
206
+
207
+ Raises:
208
+ - `ArgumentError` - Invalid hook_ids (not an array or empty)
209
+ - `Hookd::AuthenticationError` - Authentication failed
210
+ - `Hookd::ServerError` - Server error (5xx)
211
+ - `Hookd::ConnectionError` - Connection failed
212
+
213
+ **Benefits:**
214
+ - **Performance**: Reduced latency with single HTTP request
215
+ - **Efficiency**: Automatic connection reuse with HTTPX
216
+ - **Atomic**: Consistent snapshot of all hooks
217
+
115
218
  ##### `#metrics`
116
219
 
117
220
  Get server metrics (requires authentication).
data/lib/hookd/client.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
3
+ require 'httpx'
4
4
  require 'json'
5
- require 'uri'
6
5
 
7
6
  module Hookd
8
7
  # HTTP client for interacting with Hookd server
@@ -12,7 +11,13 @@ module Hookd
12
11
  def initialize(server:, token:)
13
12
  @server = server
14
13
  @token = token
15
- @uri = URI.parse(server)
14
+ @http = HTTPX.with(
15
+ headers: { 'X-API-Key' => token },
16
+ timeout: {
17
+ connect_timeout: 10,
18
+ read_timeout: 30
19
+ }
20
+ )
16
21
  end
17
22
 
18
23
  # Register one or more hooks
@@ -25,9 +30,7 @@ module Hookd
25
30
  def register(count: nil)
26
31
  body = count.nil? ? nil : { count: count }
27
32
 
28
- if count && (!count.is_a?(Integer) || count < 1)
29
- raise ArgumentError, 'count must be a positive integer'
30
- end
33
+ raise ArgumentError, 'count must be a positive integer' if count && (!count.is_a?(Integer) || count < 1)
31
34
 
32
35
  response = post('/register', body)
33
36
 
@@ -59,6 +62,27 @@ module Hookd
59
62
  raise Error, "Invalid response format: #{e.message}"
60
63
  end
61
64
 
65
+ # Poll for interactions on multiple hooks (batch)
66
+ # @param hook_ids [Array<String>] the hook IDs to poll
67
+ # @return [Hash<String, Hash>] hash mapping hook_id to results
68
+ # Results format: { "hook_id" => { interactions: [...], error: "..." } }
69
+ # @raise [Hookd::AuthenticationError] if authentication fails
70
+ # @raise [Hookd::ServerError] if server returns 5xx
71
+ # @raise [Hookd::ConnectionError] if connection fails
72
+ # @raise [ArgumentError] if hook_ids is invalid
73
+ def poll_batch(hook_ids)
74
+ validate_hook_ids(hook_ids)
75
+
76
+ url = "#{@server}/poll"
77
+ options = { headers: { 'Content-Type' => 'application/json' }, json: hook_ids }
78
+ response = @http.post(url, **options)
79
+ response_data = handle_response(response)
80
+
81
+ transform_batch_results(response_data['results'])
82
+ rescue NoMethodError => e
83
+ raise Error, "Invalid response format: #{e.message}"
84
+ end
85
+
62
86
  # Get server metrics (requires authentication)
63
87
  # @return [Hash] metrics data
64
88
  # @raise [Hookd::AuthenticationError] if authentication fails
@@ -71,43 +95,62 @@ module Hookd
71
95
  private
72
96
 
73
97
  def get(path)
74
- request = Net::HTTP::Get.new(path)
75
- request['X-API-Key'] = token
76
- execute_request(request)
98
+ url = "#{@server}#{path}"
99
+ response = @http.get(url)
100
+ handle_response(response)
77
101
  end
78
102
 
79
103
  def post(path, body = nil)
80
- request = Net::HTTP::Post.new(path)
81
- request['X-API-Key'] = token
82
- request['Content-Type'] = 'application/json'
83
- request.body = body.to_json if body
84
- execute_request(request)
104
+ url = "#{@server}#{path}"
105
+ options = { headers: { 'Content-Type' => 'application/json' } }
106
+ options[:json] = body if body
107
+
108
+ response = @http.post(url, **options)
109
+ handle_response(response)
110
+ end
111
+
112
+ def validate_hook_ids(hook_ids)
113
+ raise ArgumentError, 'hook_ids must be an array' unless hook_ids.is_a?(Array)
114
+ raise ArgumentError, 'hook_ids cannot be empty' if hook_ids.empty?
85
115
  end
86
116
 
87
- def execute_request(request)
88
- http = Net::HTTP.new(@uri.host, @uri.port)
89
- http.use_ssl = @uri.scheme == 'https'
90
- http.open_timeout = 10
91
- http.read_timeout = 30
117
+ def transform_batch_results(results)
118
+ return {} if results.nil? || !results.is_a?(Hash)
119
+
120
+ results.transform_values do |result|
121
+ next result if result['error']
122
+
123
+ interactions = result['interactions']
124
+ {
125
+ interactions: interactions&.map { |i| Interaction.from_hash(i) } || [],
126
+ error: result['error']
127
+ }
128
+ end
129
+ end
130
+
131
+ def handle_response(response)
132
+ # HTTPX returns HTTPX::ErrorResponse for connection/timeout errors
133
+ if response.is_a?(HTTPX::ErrorResponse)
134
+ error = response.error
135
+ raise ConnectionError, "Connection failed: #{error.message}"
136
+ end
92
137
 
93
- response = http.request(request)
138
+ body = response.body.to_s
94
139
 
95
- case response.code.to_i
140
+ case response.status
96
141
  when 200, 201
97
- raise Error, 'Empty response body from server' if response.body.nil? || response.body.empty?
142
+ raise Error, 'Empty response body from server' if body.nil? || body.empty?
98
143
 
99
- JSON.parse(response.body)
144
+ JSON.parse(body)
100
145
  when 401
101
- raise AuthenticationError, "Authentication failed: #{response.body}"
146
+ raise AuthenticationError, "Authentication failed: #{body}"
102
147
  when 404
103
- raise NotFoundError, "Resource not found: #{response.body}"
148
+ raise NotFoundError, "Resource not found: #{body}"
104
149
  when 500..599
105
- raise ServerError, "Server error (#{response.code}): #{response.body}"
150
+ raise ServerError, "Server error (#{response.status}): #{body}"
106
151
  else
107
- raise Error, "Unexpected response (#{response.code}): #{response.body}"
152
+ raise Error, "Unexpected response (#{response.status}): #{body}"
108
153
  end
109
- rescue SocketError, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
110
- raise ConnectionError, "Connection failed: #{e.message}"
111
154
  rescue JSON::ParserError => e
112
155
  raise Error, "Invalid JSON response: #{e.message}"
113
156
  end
data/lib/hookd/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hookd
4
- VERSION = '1.0.0'
4
+ VERSION = '1.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hookd-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua MARTINELLE
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: httpx
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
12
26
  description: Ruby client library for Hookd, a DNS/HTTP interaction server for security
13
27
  testing and debugging
14
28
  email: