hookd-client 1.2.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e020dd2f4705d56fdc57c2a7da6daba65231a9676a534037104d98cd401bcdaa
4
- data.tar.gz: 505b0f7151a9b7dfc02638b7b6c5978be6ef9832987c85b2b6b27ca7262cf8d5
3
+ metadata.gz: eeded85d2f08978163e752d64342ca5e52fd1cf1fcaade7da0305caeb53e03b5
4
+ data.tar.gz: 966b9eaf4f9d25549f0473471e446628af82c8ea160e5ebbefef518aa1a6fa08
5
5
  SHA512:
6
- metadata.gz: 32da3c04302a2e04ce0d1ea42b05f438d80e12fadfcae77de8720d1e5894d56ada4ab8b5db77d934b3bec64378d8140b747b761996931c39bd1f9b70656f94e9
7
- data.tar.gz: 78080b0c8e32c86d7634e8d137bfa8a9471e03351a4449ec6f969f12d452ffbd912d6e9ad91ef9ce832853dcd8d3c102ad567d6d3d706e1f49349158c1ebebb4
6
+ metadata.gz: 0224226f06a4996e6034caabb6182f31d9a7848262a44bd8ed0571fe898a7706f4b95c46bdb0e5617c0bc04f948fc5dc8e5bffbb46cd20de04d9bdcacbe0b2fe
7
+ data.tar.gz: 88e85eab06e2d351b6231f2769104b6d6a3320ae1cb2ccf68f432dad732a689ee5f50386ca69199534a76fb09425ddfd70f7d711b03b8e4d836e2b25415784ad
data/README.md CHANGED
@@ -130,8 +130,20 @@ hooks = client.register(count: 5)
130
130
  # => [#<Hookd::Hook id="abc123" ...>, #<Hookd::Hook id="def456" ...>, ...]
131
131
  ```
132
132
 
133
+ **Long-lived hook (survives restarts, for stored-XSS style detection):**
134
+ ```ruby
135
+ hook = client.register(ttl: '7d', metadata: { target: 'acme', field: 'profile.bio' })
136
+ # => #<Hookd::Hook id="abc123" ...>
137
+ hook.expires_at # => "2025-10-08T10:30:00Z"
138
+ hook.metadata # => {"target"=>"acme", "field"=>"profile.bio"}
139
+ ```
140
+
133
141
  Parameters:
134
142
  - `count` (Integer, optional) - Number of hooks to create (default: 1)
143
+ - `ttl` (String, optional) - Lifetime as a Go duration (`"168h"`) or day count
144
+ (`"7d"`); a value above the server's ephemeral `hook_ttl` registers a durable
145
+ long-lived hook. Omit for an ephemeral hook.
146
+ - `metadata` (Hash, optional) - Stored with the hook and echoed back on poll
135
147
 
136
148
  Returns:
137
149
  - `Hookd::Hook` object when `count` is 1 or not specified
@@ -143,6 +155,21 @@ Raises:
143
155
  - `Hookd::ServerError` - Server error (5xx)
144
156
  - `Hookd::ConnectionError` - Connection failed
145
157
 
158
+ ##### `#activity`
159
+
160
+ List the long-lived hooks that currently have pending interactions, so you can
161
+ discover which fired without polling each one; drain the details with `#poll`.
162
+
163
+ ```ruby
164
+ client.activity.each do |a|
165
+ puts "#{a.hook.id} fired #{a.pending_count} time(s), meta=#{a.hook.metadata}"
166
+ client.poll(a.hook.id)
167
+ end
168
+ # => [#<Hookd::HookActivity hook=abc123 pending=3>, ...]
169
+ ```
170
+
171
+ Returns: Array of `Hookd::HookActivity` (empty when none fired or long-lived is disabled)
172
+
146
173
  ##### `#poll(hook_id)`
147
174
 
148
175
  Poll for interactions captured by a single hook.
@@ -233,6 +260,17 @@ Attributes:
233
260
  - `http` (String) - HTTP endpoint
234
261
  - `https` (String) - HTTPS endpoint
235
262
  - `created_at` (String) - Creation timestamp
263
+ - `expires_at` (String, nil) - Expiry timestamp (long-lived hooks)
264
+ - `metadata` (Hash, nil) - Metadata attached at registration
265
+
266
+ #### `Hookd::HookActivity`
267
+
268
+ Represents a long-lived hook that has pending interactions (returned by `#activity`).
269
+
270
+ Attributes:
271
+ - `hook` (`Hookd::Hook`) - The long-lived hook that fired
272
+ - `pending_count` (Integer) - Number of interactions awaiting poll
273
+ - `last_interaction_at` (String) - Timestamp of the most recent interaction
236
274
 
237
275
  #### `Hookd::Interaction`
238
276
 
data/lib/hookd/client.rb CHANGED
@@ -22,25 +22,20 @@ module Hookd
22
22
 
23
23
  # Register one or more hooks
24
24
  # @param count [Integer, nil] number of hooks to register (default: 1)
25
+ # @param ttl [String, nil] lifetime as a Go duration ("168h") or day count
26
+ # ("7d"); a value above the server's ephemeral hook_ttl registers a durable
27
+ # long-lived hook. Omit for an ephemeral hook.
28
+ # @param metadata [Hash, nil] arbitrary data stored with the hook and echoed
29
+ # back on poll
25
30
  # @return [Hookd::Hook, Array<Hookd::Hook>] single hook or array of hooks
26
31
  # @raise [Hookd::AuthenticationError] if authentication fails
27
32
  # @raise [Hookd::ServerError] if server returns 5xx
28
33
  # @raise [Hookd::ConnectionError] if connection fails
29
34
  # @raise [ArgumentError] if count is invalid
30
- def register(count: nil)
31
- body = count.nil? ? nil : { count: count }
32
-
35
+ def register(count: nil, ttl: nil, metadata: nil)
33
36
  raise ArgumentError, 'count must be a positive integer' if count && (!count.is_a?(Integer) || count < 1)
34
37
 
35
- response = post('/register', body)
36
-
37
- # Single hook response (backward compatible)
38
- return Hook.from_hash(response) if response.key?('id')
39
-
40
- # Multiple hooks response
41
- return [] if response['hooks'].nil? || response['hooks'].empty?
42
-
43
- response['hooks'].map { |h| Hook.from_hash(h) }
38
+ parse_register_response(post('/register', register_body(count, ttl, metadata)))
44
39
  end
45
40
 
46
41
  # Poll for interactions on a hook
@@ -92,8 +87,45 @@ module Hookd
92
87
  get('/metrics')
93
88
  end
94
89
 
90
+ # List long-lived hooks that currently have pending interactions, so you can
91
+ # discover which of your long-lived hooks fired without polling each one.
92
+ # Drain the details with #poll. Returns an empty array when none have fired
93
+ # (or the server has long-lived hooks disabled).
94
+ # @return [Array<Hookd::HookActivity>]
95
+ # @raise [Hookd::AuthenticationError] if authentication fails
96
+ # @raise [Hookd::ServerError] if server returns 5xx
97
+ # @raise [Hookd::ConnectionError] if connection fails
98
+ def activity
99
+ response = get('/activity')
100
+
101
+ hooks = response['hooks']
102
+ return [] if hooks.nil? || hooks.empty? || !hooks.is_a?(Array)
103
+
104
+ hooks.map { |h| HookActivity.from_hash(h) }
105
+ rescue NoMethodError => e
106
+ raise Error, "Invalid response format: #{e.message}"
107
+ end
108
+
95
109
  private
96
110
 
111
+ def register_body(count, ttl, metadata)
112
+ body = {}
113
+ body[:count] = count unless count.nil?
114
+ body[:ttl] = ttl unless ttl.nil?
115
+ body[:metadata] = metadata unless metadata.nil?
116
+ body.empty? ? nil : body
117
+ end
118
+
119
+ def parse_register_response(response)
120
+ # Single hook response (backward compatible)
121
+ return Hook.from_hash(response) if response.key?('id')
122
+
123
+ # Multiple hooks response
124
+ return [] if response['hooks'].nil? || response['hooks'].empty?
125
+
126
+ response['hooks'].map { |h| Hook.from_hash(h) }
127
+ end
128
+
97
129
  def get(path)
98
130
  url = "#{@server}#{path}"
99
131
  response = @http.get(url)
data/lib/hookd/hook.rb CHANGED
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hookd
4
- # Represents a registered hook with DNS and HTTP endpoints
4
+ # Represents a registered hook with DNS and HTTP endpoints. expires_at and
5
+ # metadata are populated for long-lived hooks (registered with a ttl) and nil
6
+ # otherwise.
5
7
  class Hook
6
- attr_reader :id, :dns, :http, :https, :created_at
8
+ attr_reader :id, :dns, :http, :https, :created_at, :expires_at, :metadata
7
9
 
8
- def initialize(id:, dns:, http:, https:, created_at:)
10
+ def initialize(id:, dns:, http:, https:, created_at:, expires_at: nil, metadata: nil)
9
11
  @id = id
10
12
  @dns = dns
11
13
  @http = http
12
14
  @https = https
13
15
  @created_at = created_at
16
+ @expires_at = expires_at
17
+ @metadata = metadata
14
18
  end
15
19
 
16
20
  # Create a Hook from API response hash
@@ -22,7 +26,9 @@ module Hookd
22
26
  dns: hash['dns'],
23
27
  http: hash['http'],
24
28
  https: hash['https'],
25
- created_at: hash['created_at']
29
+ created_at: hash['created_at'],
30
+ expires_at: hash['expires_at'],
31
+ metadata: hash['metadata']
26
32
  )
27
33
  end
28
34
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hookd
4
+ # Summarises a long-lived hook that currently has pending interactions,
5
+ # returned by Client#activity.
6
+ class HookActivity
7
+ attr_reader :hook, :pending_count, :last_interaction_at
8
+
9
+ def initialize(hook:, pending_count:, last_interaction_at:)
10
+ @hook = hook
11
+ @pending_count = pending_count
12
+ @last_interaction_at = last_interaction_at
13
+ end
14
+
15
+ # Create a HookActivity from an API response hash
16
+ def self.from_hash(hash)
17
+ raise ArgumentError, "Invalid hash: expected Hash, got #{hash.class}" unless hash.is_a?(Hash)
18
+
19
+ new(
20
+ hook: Hook.from_hash(hash['hook']),
21
+ pending_count: hash['pending_count'],
22
+ last_interaction_at: hash['last_interaction_at']
23
+ )
24
+ end
25
+
26
+ def to_s
27
+ "#<Hookd::HookActivity hook=#{hook.id} pending=#{pending_count}>"
28
+ end
29
+ end
30
+ 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.2.2'
4
+ VERSION = '1.3.0'
5
5
  end
data/lib/hookd.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'hookd/version'
4
4
  require_relative 'hookd/error'
5
5
  require_relative 'hookd/hook'
6
+ require_relative 'hookd/hook_activity'
6
7
  require_relative 'hookd/interaction'
7
8
  require_relative 'hookd/client'
8
9
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hookd-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua MARTINELLE
@@ -36,6 +36,7 @@ files:
36
36
  - lib/hookd/client.rb
37
37
  - lib/hookd/error.rb
38
38
  - lib/hookd/hook.rb
39
+ - lib/hookd/hook_activity.rb
39
40
  - lib/hookd/interaction.rb
40
41
  - lib/hookd/version.rb
41
42
  homepage: https://github.com/JoshuaMart/Hookd