nahook 0.1.1 → 0.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: 06f9b63028d18b1d6b6c6c1c89a1c2583c3407faf379e66cc21f8a15a4558829
4
- data.tar.gz: 96c50392e9d71244838c5726168117481310e8f48b269ecb4305b97bd253e057
3
+ metadata.gz: f3bd344552a65f2ef26b92e4578433a42c3681a8a9f524391cf062973273a416
4
+ data.tar.gz: 73d4cfa42d44d9beaafdf4c94fabad2f4dba8c5224194d73c5af64d6e799ae01
5
5
  SHA512:
6
- metadata.gz: 32e6ce367ebaa16276a0580ef1caea905e0926cf312223b7fdd6e2c0ce9628ba4498c4d15e040e336600d7d88c204c1d595ee5e12d34491b821b6bd875a859bf
7
- data.tar.gz: 3fd57195f0019f5bfe8347a60fc0a317a0fc15e5b29525b55cf2de7e628ceb7205720df0d1dc940ee749756ef7901ad981a69325312bfd23ba05438fa22c6375
6
+ metadata.gz: f6ae693721c3e083de6eb51e11ac1da781f5478549c73a723c23a235d1534f3c1b6108e17c43151988d54429aea5cc5e9b4aabb94ebea2fcdac1f54c942e629c
7
+ data.tar.gz: 1d4570d3add3fd70744c8e03469d02ffe60612b96a59879ef86a0016c053fb25b7ee2be06730c9e4047dfa4bd8d502eb7d2f88e7ad3c2a8af0fcce380c7b13aa
data/README.md CHANGED
@@ -88,6 +88,56 @@ session = mgmt.portal_sessions.create("ws_abc123", "app_jkl012")
88
88
  puts session["url"] # Redirect your customer here
89
89
  ```
90
90
 
91
+ ### Deliveries
92
+
93
+ Read access to a workspace's webhook deliveries. There is no create/update/delete on this resource — deliveries are produced by the ingestion path and observed through these methods.
94
+
95
+ ```ruby
96
+ # List deliveries for an endpoint, newest-first, cursor-paginated.
97
+ # `next_cursor` is OPAQUE — pass it back verbatim to fetch the next page.
98
+ page = mgmt.deliveries.list("ws_abc123", "ep_def456", limit: 50)
99
+ page.data.each { |d| puts "#{d['id']} #{d['status']}" }
100
+
101
+ while page.next_cursor
102
+ page = mgmt.deliveries.list("ws_abc123", "ep_def456", cursor: page.next_cursor)
103
+ page.data.each { |d| puts "#{d['id']} #{d['status']}" }
104
+ end
105
+
106
+ # Filter by status. Valid values: "pending", "delivering", "delivered",
107
+ # "scheduled_retry", "failed", "dead_letter".
108
+ failed = mgmt.deliveries.list("ws_abc123", "ep_def456", status: "failed")
109
+
110
+ # Fetch a single delivery (metadata only).
111
+ delivery = mgmt.deliveries.get("ws_abc123", "del_xyz")
112
+ puts delivery["status"] # => "delivered"
113
+ puts delivery["totalAttempts"] # => 1
114
+ puts delivery["hasPayload"] # => true
115
+
116
+ # Fetch with the payload envelope. The envelope is a tagged hash whose
117
+ # "status" is one of: "available", "forbidden", "processing", "not_found",
118
+ # "error". The endpoint stays HTTP 200 for all 5 -- the envelope status
119
+ # carries access-level reality, so do NOT rescue on the non-"available" ones.
120
+ delivery = mgmt.deliveries.get("ws_abc123", "del_xyz", include_payload: true)
121
+ case delivery["payload"]["status"]
122
+ when "available"
123
+ body = delivery["payload"]["data"] # decoded JSON
124
+ content_type = delivery["payload"]["contentType"] # "application/json"
125
+ when "forbidden"
126
+ # Workspace plan does not include payload storage.
127
+ when "processing"
128
+ # Delivery still in flight; payload may be racing the read.
129
+ when "not_found"
130
+ # Terminal delivery without stored payload (older row, or plan was lower at ingest).
131
+ when "error"
132
+ # Transient infrastructure failure -- safe to retry.
133
+ end
134
+
135
+ # List attempts for a delivery in chronological order (oldest first).
136
+ # The attempt "status" is an opaque string, not an enum.
137
+ attempts = mgmt.deliveries.get_attempts("ws_abc123", "del_xyz")
138
+ attempts.each { |a| puts "##{a['attemptNumber']} #{a['status']} #{a['responseStatusCode']}" }
139
+ ```
140
+
91
141
  ## Client Options
92
142
 
93
143
  ### Nahook::Client
@@ -111,6 +161,40 @@ client = Nahook::Client.new("nhk_us_...", base_url: "http://localhost:3001")
111
161
 
112
162
  For unit tests, mock the SDK client at the dependency injection boundary. For integration tests, override the base URL to point at a local server.
113
163
 
164
+ ### Custom HTTP adapter / Faraday connection
165
+
166
+ The SDK ships with the `:net_http_persistent` Faraday adapter by default, which keeps a per-thread TCP/TLS pool — back-to-back `send` / `trigger` calls reuse the same connection and skip the DNS + TCP + TLS handshake. For most apps that's all you need.
167
+
168
+ Two escape hatches when you want more control:
169
+
170
+ **Swap the adapter.** Useful for high-throughput workloads on `:typhoeus` (curl-multi) without building a full Faraday connection:
171
+
172
+ ```ruby
173
+ require "faraday/typhoeus" # add the gem to your Gemfile
174
+
175
+ client = Nahook::Client.new("nhk_us_...", adapter: :typhoeus)
176
+ ```
177
+
178
+ Whatever adapter you name must be available — Faraday 2 split adapters into separate gems (`faraday-typhoeus`, `faraday-net_http`, etc.).
179
+
180
+ **Supply a fully-configured Faraday connection.** When you need middleware (Datadog / OpenTelemetry instrumentation), a custom proxy, mTLS, request retries beyond what the SDK ships, etc.:
181
+
182
+ ```ruby
183
+ conn = Faraday.new(url: "https://us.api.nahook.com") do |f|
184
+ f.options.timeout = 15
185
+ f.options.open_timeout = 5
186
+ # Plug in your tracing/observability middleware. The exact symbol/class
187
+ # depends on the gem — e.g. `Datadog::Tracing::Contrib::Faraday::Middleware`
188
+ # from `ddtrace`, or `OpenTelemetry::Instrumentation::Faraday::Middlewares::TracerMiddleware`.
189
+ # f.use SomeTracingMiddleware
190
+ f.adapter :net_http_persistent
191
+ end
192
+
193
+ client = Nahook::Client.new("nhk_us_...", connection: conn)
194
+ ```
195
+
196
+ When `connection:` is supplied, `base_url`, `timeout_ms`, and `adapter` are ignored — the connection you pass in is used verbatim. The same `adapter:` and `connection:` kwargs are accepted by `Nahook::Management.new`.
197
+
114
198
  ### Nahook::Management
115
199
 
116
200
  ```ruby
data/lib/nahook/client.rb CHANGED
@@ -23,20 +23,33 @@ module Nahook
23
23
  # @param base_url [String] API base URL
24
24
  # @param timeout_ms [Integer] request timeout in milliseconds (default: 30000)
25
25
  # @param retries [Integer] number of retry attempts for retryable errors
26
+ # @param adapter [Symbol, nil] Faraday adapter to use (defaults to
27
+ # `:net_http_persistent` for keep-alive). Useful for swapping to e.g.
28
+ # `:typhoeus` for high-throughput workloads without building a full
29
+ # Faraday connection.
30
+ # @param connection [Faraday::Connection, nil] a fully-configured Faraday
31
+ # connection. When supplied, `base_url`, `timeout_ms`, and `adapter`
32
+ # are ignored — the caller owns connection configuration (custom
33
+ # middleware, instrumentation, proxies, mTLS, etc.).
26
34
  # @raise [ArgumentError] if the API key does not start with "nhk_"
27
- def initialize(api_key, base_url: nil, timeout_ms: HttpClient::DEFAULT_TIMEOUT_MS, retries: 0)
35
+ def initialize(api_key, base_url: nil, timeout_ms: HttpClient::DEFAULT_TIMEOUT_MS,
36
+ retries: 0, adapter: nil, connection: nil)
28
37
  unless api_key.start_with?("nhk_")
29
38
  raise ArgumentError, "Invalid API key: must start with 'nhk_'"
30
39
  end
31
40
 
32
41
  resolved_url = base_url || HttpClient.resolve_base_url(api_key)
33
42
 
34
- @http = HttpClient.new(
43
+ http_kwargs = {
35
44
  token: api_key,
36
45
  base_url: resolved_url,
37
46
  timeout_ms: timeout_ms,
38
- retries: retries
39
- )
47
+ retries: retries,
48
+ }
49
+ http_kwargs[:adapter] = adapter if adapter
50
+ http_kwargs[:connection] = connection if connection
51
+
52
+ @http = HttpClient.new(**http_kwargs)
40
53
  end
41
54
 
42
55
  # Send a payload to a specific endpoint.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "faraday"
4
+ require "faraday/net_http_persistent"
4
5
  require "json"
5
6
  require "cgi"
6
7
 
@@ -17,6 +18,12 @@ module Nahook
17
18
  BASE_DELAY_MS = 500
18
19
  MAX_DELAY_MS = 10_000
19
20
 
21
+ # The default Faraday adapter. `net_http_persistent` keeps a per-thread
22
+ # TCP/TLS pool so back-to-back requests skip the full handshake. Callers
23
+ # can override via the `adapter:` kwarg, or bypass adapter selection
24
+ # entirely by passing a pre-built `connection:`.
25
+ DEFAULT_ADAPTER = :net_http_persistent
26
+
20
27
  REGION_BASE_URLS = {
21
28
  "us" => "https://us.api.nahook.com",
22
29
  "eu" => "https://eu.api.nahook.com",
@@ -36,16 +43,30 @@ module Nahook
36
43
  # @param base_url [String] API base URL
37
44
  # @param timeout_ms [Integer] request timeout in milliseconds (default: 30000)
38
45
  # @param retries [Integer] number of retry attempts for retryable errors
39
- def initialize(token:, base_url: DEFAULT_BASE_URL, timeout_ms: DEFAULT_TIMEOUT_MS, retries: 0)
40
- @token = token
41
- @retries = retries
42
- @timeout_ms = timeout_ms
43
-
44
- timeout_secs = timeout_ms / 1000.0
45
- @conn = Faraday.new(url: base_url.chomp("/")) do |f|
46
- f.options.timeout = timeout_secs
47
- f.options.open_timeout = timeout_secs
48
- f.adapter Faraday.default_adapter
46
+ # @param adapter [Symbol] Faraday adapter to use when no custom connection
47
+ # is supplied. Defaults to `:net_http_persistent` (keep-alive). Other
48
+ # options the caller can pass: `:net_http`, `:typhoeus`, `:patron`, etc.
49
+ # Whatever adapter is named must be available — Faraday 2 split adapters
50
+ # into separate gems, so e.g. `:typhoeus` requires `gem "faraday-typhoeus"`.
51
+ # @param connection [Faraday::Connection, nil] a fully-configured Faraday
52
+ # connection to use verbatim. When supplied, `base_url`, `timeout_ms`,
53
+ # and `adapter` are ignored — the caller owns connection configuration
54
+ # (custom middleware, instrumentation, proxies, mTLS, etc.).
55
+ def initialize(token:, base_url: DEFAULT_BASE_URL, timeout_ms: DEFAULT_TIMEOUT_MS,
56
+ retries: 0, adapter: DEFAULT_ADAPTER, connection: nil)
57
+ @token = token
58
+ @retries = retries
59
+
60
+ if connection
61
+ @conn = connection
62
+ # Prefer the supplied connection's own timeout so `TimeoutError` reports
63
+ # the value the caller actually configured. Faraday options.timeout is
64
+ # in seconds — convert. Fall back to the constructor kwarg if unset.
65
+ conn_timeout_secs = connection.options.timeout
66
+ @timeout_ms = conn_timeout_secs ? (conn_timeout_secs * 1000).to_i : timeout_ms
67
+ else
68
+ @timeout_ms = timeout_ms
69
+ @conn = build_default_connection(base_url, timeout_ms, adapter)
49
70
  end
50
71
  end
51
72
 
@@ -160,5 +181,14 @@ module Nahook
160
181
  else false
161
182
  end
162
183
  end
184
+
185
+ def build_default_connection(base_url, timeout_ms, adapter)
186
+ timeout_secs = timeout_ms / 1000.0
187
+ Faraday.new(url: base_url.chomp("/")) do |f|
188
+ f.options.timeout = timeout_secs
189
+ f.options.open_timeout = timeout_secs
190
+ f.adapter adapter
191
+ end
192
+ end
163
193
  end
164
194
  end
@@ -41,16 +41,29 @@ module Nahook
41
41
  # @return [Resources::Environments]
42
42
  attr_reader :environments
43
43
 
44
+ # @return [Resources::Deliveries]
45
+ attr_reader :deliveries
46
+
44
47
  # @param token [String] management token (must start with "nhm_")
45
48
  # @param base_url [String] API base URL
46
49
  # @param timeout_ms [Integer] request timeout in milliseconds (default: 30000)
50
+ # @param adapter [Symbol, nil] Faraday adapter to use (defaults to
51
+ # `:net_http_persistent` for keep-alive). See {Client#initialize}.
52
+ # @param connection [Faraday::Connection, nil] a fully-configured Faraday
53
+ # connection. When supplied, `base_url`, `timeout_ms`, and `adapter`
54
+ # are ignored.
47
55
  # @raise [ArgumentError] if the token does not start with "nhm_"
48
- def initialize(token, base_url: HttpClient::DEFAULT_BASE_URL, timeout_ms: HttpClient::DEFAULT_TIMEOUT_MS)
56
+ def initialize(token, base_url: HttpClient::DEFAULT_BASE_URL, timeout_ms: HttpClient::DEFAULT_TIMEOUT_MS,
57
+ adapter: nil, connection: nil)
49
58
  unless token.start_with?("nhm_")
50
59
  raise ArgumentError, "Invalid management token: must start with 'nhm_'"
51
60
  end
52
61
 
53
- http = HttpClient.new(token: token, base_url: base_url, timeout_ms: timeout_ms)
62
+ http_kwargs = { token: token, base_url: base_url, timeout_ms: timeout_ms }
63
+ http_kwargs[:adapter] = adapter if adapter
64
+ http_kwargs[:connection] = connection if connection
65
+
66
+ http = HttpClient.new(**http_kwargs)
54
67
 
55
68
  @endpoints = Resources::Endpoints.new(http)
56
69
  @event_types = Resources::EventTypes.new(http)
@@ -58,6 +71,7 @@ module Nahook
58
71
  @subscriptions = Resources::Subscriptions.new(http)
59
72
  @portal_sessions = Resources::PortalSessions.new(http)
60
73
  @environments = Resources::Environments.new(http)
74
+ @deliveries = Resources::Deliveries.new(http)
61
75
  end
62
76
  end
63
77
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Nahook
6
+ # Generic paginated result returned by cursor-paginated list endpoints.
7
+ #
8
+ # @!attribute [rw] data
9
+ # @return [Array<Hash>] the page of results
10
+ # @!attribute [rw] next_cursor
11
+ # @return [String, nil] opaque cursor for the next page, or nil if this is
12
+ # the last page. Pass back verbatim into the next list call.
13
+ PaginatedResult = Struct.new(:data, :next_cursor)
14
+
15
+ module Resources
16
+ # Read access to a workspace's webhook deliveries via the Management API.
17
+ #
18
+ # All methods are paginated or single-resource reads -- this resource has
19
+ # no create/update/delete operations. Deliveries are produced by the
20
+ # ingestion path and consumed by the worker; the Management API exposes
21
+ # only their observable state.
22
+ #
23
+ # @example
24
+ # mgmt = Nahook::Management.new("nhm_token")
25
+ #
26
+ # # List a page of deliveries for an endpoint
27
+ # page = mgmt.deliveries.list("ws_abc123", "ep_def456", limit: 50)
28
+ # page.data.each { |d| puts d["id"] }
29
+ # next_page = mgmt.deliveries.list("ws_abc123", "ep_def456", cursor: page.next_cursor)
30
+ #
31
+ # # Fetch a single delivery (metadata only)
32
+ # delivery = mgmt.deliveries.get("ws_abc123", "del_xyz")
33
+ #
34
+ # # Fetch with payload envelope
35
+ # delivery = mgmt.deliveries.get("ws_abc123", "del_xyz", include_payload: true)
36
+ #
37
+ # # List attempts (chronological order)
38
+ # attempts = mgmt.deliveries.get_attempts("ws_abc123", "del_xyz")
39
+ class Deliveries
40
+ # @api private
41
+ # @param http [Nahook::HttpClient]
42
+ def initialize(http)
43
+ @http = http
44
+ end
45
+
46
+ # List deliveries for an endpoint, newest-first, with opaque cursor pagination.
47
+ #
48
+ # @param workspace_id [String] the workspace public ID
49
+ # @param endpoint_id [String] the endpoint public ID
50
+ # @param limit [Integer, nil] page size, server-capped (default 50, max 100)
51
+ # @param cursor [String, nil] opaque cursor from a previous response's
52
+ # {PaginatedResult#next_cursor}. Pass through unchanged.
53
+ # @param status [String, nil] filter by delivery status. One of:
54
+ # "pending", "delivering", "delivered", "scheduled_retry", "failed",
55
+ # "dead_letter".
56
+ # @return [PaginatedResult] paginated result whose +data+ is an array of
57
+ # delivery hashes and +next_cursor+ is a String or nil.
58
+ def list(workspace_id, endpoint_id, limit: nil, cursor: nil, status: nil)
59
+ raw = @http.request(
60
+ method: :get,
61
+ path: "/management/v1/workspaces/#{e(workspace_id)}/endpoints/#{e(endpoint_id)}/deliveries",
62
+ query: { "limit" => limit, "cursor" => cursor, "status" => status }
63
+ )
64
+ PaginatedResult.new(raw["deliveries"] || [], raw["nextCursor"])
65
+ end
66
+
67
+ # Get a single delivery's metadata, optionally including the payload envelope.
68
+ #
69
+ # When +include_payload+ is true, the response includes a "payload"
70
+ # envelope whose "status" is one of: "available", "forbidden",
71
+ # "processing", "not_found", "error". The endpoint stays 200 for all 5 --
72
+ # the envelope status carries access-level reality. This method does NOT
73
+ # raise on "forbidden"/"processing"/"not_found"/"error".
74
+ #
75
+ # @param workspace_id [String] the workspace public ID
76
+ # @param delivery_id [String] the delivery public ID (starts with "del_")
77
+ # @param include_payload [Boolean] if true, sends ?include=payload and
78
+ # the response includes a "payload" envelope (default false).
79
+ # @return [Hash] the delivery; includes a "payload" envelope when
80
+ # +include_payload+ is true.
81
+ def get(workspace_id, delivery_id, include_payload: false)
82
+ query = include_payload ? { "include" => "payload" } : nil
83
+ @http.request(
84
+ method: :get,
85
+ path: "/management/v1/workspaces/#{e(workspace_id)}/deliveries/#{e(delivery_id)}",
86
+ query: query
87
+ )
88
+ end
89
+
90
+ # List delivery attempts for a single delivery, in chronological order
91
+ # (oldest first).
92
+ #
93
+ # The attempt "status" field is an opaque string ("failed", "success",
94
+ # etc.) -- treat it as a string, not an enum.
95
+ #
96
+ # @param workspace_id [String] the workspace public ID
97
+ # @param delivery_id [String] the delivery public ID (starts with "del_")
98
+ # @return [Array<Hash>] attempts in chronological order
99
+ def get_attempts(workspace_id, delivery_id)
100
+ @http.request(
101
+ method: :get,
102
+ path: "/management/v1/workspaces/#{e(workspace_id)}/deliveries/#{e(delivery_id)}/attempts"
103
+ )
104
+ end
105
+
106
+ private
107
+
108
+ def e(value)
109
+ CGI.escape(value.to_s)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nahook
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/nahook.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "nahook/resources/applications"
11
11
  require_relative "nahook/resources/subscriptions"
12
12
  require_relative "nahook/resources/portal_sessions"
13
13
  require_relative "nahook/resources/environments"
14
+ require_relative "nahook/resources/deliveries"
14
15
 
15
16
  # Official Ruby SDK for the Nahook webhook platform.
16
17
  #
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nahook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nahook
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-25 00:00:00.000000000 Z
11
+ date: 2026-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-net_http_persistent
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
27
41
  description: Ruby client for sending webhooks and managing resources through the Nahook
28
42
  API. Supports direct endpoint delivery, fan-out by event type, batch operations,
29
43
  and full management API access.
@@ -41,6 +55,7 @@ files:
41
55
  - lib/nahook/http_client.rb
42
56
  - lib/nahook/management.rb
43
57
  - lib/nahook/resources/applications.rb
58
+ - lib/nahook/resources/deliveries.rb
44
59
  - lib/nahook/resources/endpoints.rb
45
60
  - lib/nahook/resources/environments.rb
46
61
  - lib/nahook/resources/event_types.rb