nahook 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dff420cf119a0ff62244803d4fb0897bc403a42172a27f187efecbd2b79d9eb6
4
+ data.tar.gz: 2eb2ee2daa42e790da927002db9adb91b2034804fd8fd71b64bf9123766fc97e
5
+ SHA512:
6
+ metadata.gz: a30168ab2246994559832746f45bb714b3f7d120ee0a00d4c646983a19a0a92237bf4db7307c53504ef81e76977e83fb8dab615414e83218757ae861ec445005
7
+ data.tar.gz: b9a956794a9710d356e09efb57f0fc2d12ed22f0c95e515c1f992983be72c58be3816bb6fefdee21d0a183a4d56a809d701a039910a69c3ff069e0421d6f9dc0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nahook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # Nahook Ruby SDK
2
+
3
+ Official Ruby SDK for the [Nahook](https://nahook.com) webhook platform. Send webhooks, fan-out by event type, and manage resources programmatically.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "nahook"
11
+ ```
12
+
13
+ Or install directly:
14
+
15
+ ```bash
16
+ gem install nahook
17
+ ```
18
+
19
+ **Requirements:** Ruby 3.0+
20
+
21
+ ## Quick Start
22
+
23
+ ### Sending Webhooks (Client)
24
+
25
+ ```ruby
26
+ require "nahook"
27
+
28
+ client = Nahook::Client.new("nhk_us_your_api_key")
29
+
30
+ # Send to a specific endpoint
31
+ result = client.send("ep_abc123", payload: { order_id: "12345", status: "paid" })
32
+ puts result["deliveryId"] # => "del_..."
33
+
34
+ # Fan-out by event type (delivers to all subscribed endpoints)
35
+ result = client.trigger("order.paid", payload: { order_id: "12345" })
36
+ puts result["deliveryIds"] # => ["del_1", "del_2"]
37
+
38
+ # With metadata
39
+ client.trigger("order.paid",
40
+ payload: { order_id: "12345" },
41
+ metadata: { "source" => "checkout" }
42
+ )
43
+ ```
44
+
45
+ ### Managing Resources (Management)
46
+
47
+ ```ruby
48
+ mgmt = Nahook::Management.new("nhm_your_management_token")
49
+
50
+ # Endpoints
51
+ endpoints = mgmt.endpoints.list("ws_abc123")
52
+ endpoint = mgmt.endpoints.create("ws_abc123",
53
+ url: "https://example.com/webhook",
54
+ type: "webhook",
55
+ description: "Production webhook"
56
+ )
57
+ mgmt.endpoints.update("ws_abc123", endpoint["id"], is_active: false)
58
+ mgmt.endpoints.delete("ws_abc123", endpoint["id"])
59
+
60
+ # Event Types
61
+ mgmt.event_types.create("ws_abc123", name: "order.paid", description: "Fired when an order is paid")
62
+ types = mgmt.event_types.list("ws_abc123")
63
+
64
+ # Applications
65
+ app = mgmt.applications.create("ws_abc123", name: "Acme Corp", external_id: "acme_123")
66
+ mgmt.applications.list("ws_abc123", limit: 10, offset: 0)
67
+ mgmt.applications.list_endpoints("ws_abc123", app["id"])
68
+ mgmt.applications.create_endpoint("ws_abc123", app["id"], url: "https://acme.com/hook")
69
+
70
+ # Subscriptions
71
+ mgmt.subscriptions.create("ws_abc123", "ep_def456", event_type_ids: ["evt_ghi789"])
72
+ mgmt.subscriptions.list("ws_abc123", "ep_def456")
73
+ mgmt.subscriptions.delete("ws_abc123", "ep_def456", "evt_ghi789")
74
+
75
+ # Environments
76
+ env = mgmt.environments.create("ws_abc123", name: "Staging", slug: "staging")
77
+ mgmt.environments.list("ws_abc123")
78
+ mgmt.environments.get("ws_abc123", env["id"])
79
+ mgmt.environments.update("ws_abc123", env["id"], name: "Pre-production")
80
+ mgmt.environments.delete("ws_abc123", env["id"])
81
+
82
+ # Event Type Visibility
83
+ mgmt.environments.list_event_type_visibility("ws_abc123", "env_abc123")
84
+ mgmt.environments.set_event_type_visibility("ws_abc123", "env_abc123", "evt_abc123", published: true)
85
+
86
+ # Portal Sessions
87
+ session = mgmt.portal_sessions.create("ws_abc123", "app_jkl012")
88
+ puts session["url"] # Redirect your customer here
89
+ ```
90
+
91
+ ## Client Options
92
+
93
+ ### Nahook::Client
94
+
95
+ ```ruby
96
+ client = Nahook::Client.new("nhk_us_...",
97
+ timeout_ms: 30_000, # milliseconds, default
98
+ retries: 3 # retry on 5xx/429/network errors
99
+ )
100
+ ```
101
+
102
+ ### Configuration
103
+
104
+ The SDK automatically routes requests to the correct regional API based on your API key prefix (`nhk_us_...` -> US, `nhk_eu_...` -> EU, `nhk_ap_...` -> Asia Pacific). No configuration needed.
105
+
106
+ To override the base URL (for testing or local development):
107
+
108
+ ```ruby
109
+ client = Nahook::Client.new("nhk_us_...", base_url: "http://localhost:3001")
110
+ ```
111
+
112
+ 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
+
114
+ ### Nahook::Management
115
+
116
+ ```ruby
117
+ mgmt = Nahook::Management.new("nhm_...",
118
+ timeout_ms: 30_000 # milliseconds, default
119
+ )
120
+ # Note: Management does not support retries
121
+ ```
122
+
123
+ ## Batch Operations
124
+
125
+ ```ruby
126
+ # Send to multiple endpoints (max 20)
127
+ result = client.send_batch([
128
+ { endpoint_id: "ep_abc", payload: { order: 1 } },
129
+ { endpoint_id: "ep_def", payload: { order: 2 }, idempotency_key: "key-2" }
130
+ ])
131
+
132
+ # Fan-out multiple event types (max 20)
133
+ result = client.trigger_batch([
134
+ { event_type: "order.paid", payload: { order_id: "123" } },
135
+ { event_type: "user.created", payload: { user_id: "456" }, metadata: { "source" => "api" } }
136
+ ])
137
+ ```
138
+
139
+ ## Idempotency
140
+
141
+ The `send` method auto-generates a UUID idempotency key if you don't provide one:
142
+
143
+ ```ruby
144
+ # Auto-generated idempotency key
145
+ client.send("ep_abc", payload: { order: 1 })
146
+
147
+ # Explicit idempotency key
148
+ client.send("ep_abc", payload: { order: 1 }, idempotency_key: "order-1-v1")
149
+ ```
150
+
151
+ ## Error Handling
152
+
153
+ ```ruby
154
+ begin
155
+ client.send("ep_abc", payload: { test: true })
156
+ rescue Nahook::APIError => e
157
+ puts e.message # Human-readable message
158
+ puts e.status # HTTP status code
159
+ puts e.code # Machine-readable error code
160
+ puts e.retryable? # true for 5xx and 429
161
+ puts e.auth_error? # true for 401, or 403 with token_disabled
162
+ puts e.not_found? # true for 404
163
+ puts e.rate_limited? # true for 429
164
+ puts e.retry_after # Retry-After header value (seconds), if present
165
+ rescue Nahook::NetworkError => e
166
+ puts e.message # "Network error: ..."
167
+ puts e.original_error # Original exception
168
+ rescue Nahook::TimeoutError => e
169
+ puts e.message # "Request timed out after 30000ms"
170
+ puts e.timeout_ms # Timeout in milliseconds
171
+ rescue Nahook::Error => e
172
+ # Catch-all for any SDK error
173
+ end
174
+ ```
175
+
176
+ ## Retry Logic
177
+
178
+ When `retries` is configured on `Nahook::Client`, the SDK automatically retries on:
179
+
180
+ - HTTP 5xx responses
181
+ - HTTP 429 (rate limited) -- respects `Retry-After` header
182
+ - Network connection failures
183
+ - Request timeouts
184
+
185
+ Retry delay uses exponential backoff with full jitter (base 500ms, max 10s).
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Nahook
6
+ # Client for sending webhook payloads through the Nahook ingestion API.
7
+ #
8
+ # Supports sending to specific endpoints, fan-out by event type,
9
+ # and batch operations. Includes configurable retry with exponential backoff.
10
+ #
11
+ # @example Basic usage
12
+ # client = Nahook::Client.new("nhk_us_your_api_key")
13
+ # client.send("ep_abc123", payload: { order_id: "12345" })
14
+ #
15
+ # @example With options
16
+ # client = Nahook::Client.new("nhk_us_your_api_key",
17
+ # base_url: "https://custom.nahook.com",
18
+ # timeout_ms: 15_000,
19
+ # retries: 3
20
+ # )
21
+ class Client
22
+ # @param api_key [String] API key (must start with "nhk_")
23
+ # @param base_url [String] API base URL
24
+ # @param timeout_ms [Integer] request timeout in milliseconds (default: 30000)
25
+ # @param retries [Integer] number of retry attempts for retryable errors
26
+ # @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)
28
+ unless api_key.start_with?("nhk_")
29
+ raise ArgumentError, "Invalid API key: must start with 'nhk_'"
30
+ end
31
+
32
+ resolved_url = base_url || HttpClient.resolve_base_url(api_key)
33
+
34
+ @http = HttpClient.new(
35
+ token: api_key,
36
+ base_url: resolved_url,
37
+ timeout_ms: timeout_ms,
38
+ retries: retries
39
+ )
40
+ end
41
+
42
+ # Send a payload to a specific endpoint.
43
+ #
44
+ # @param endpoint_id [String] the endpoint public ID (e.g. "ep_abc123")
45
+ # @param payload [Hash] the webhook payload
46
+ # @param idempotency_key [String, nil] optional idempotency key (auto-generated if omitted)
47
+ # @return [Hash] response with "deliveryId", "idempotencyKey", and "status" keys
48
+ # @raise [APIError] on API error responses
49
+ # @raise [NetworkError] on connection failures
50
+ # @raise [TimeoutError] on request timeout
51
+ def send(endpoint_id, payload:, idempotency_key: nil)
52
+ key = idempotency_key || SecureRandom.uuid
53
+
54
+ @http.request(
55
+ method: :post,
56
+ path: "/api/ingest/#{CGI.escape(endpoint_id)}",
57
+ body: {
58
+ "payload" => payload,
59
+ "idempotencyKey" => key
60
+ }
61
+ )
62
+ end
63
+
64
+ # Fan-out a payload by event type to all subscribed endpoints.
65
+ #
66
+ # @param event_type [String] the event type name (e.g. "order.paid")
67
+ # @param payload [Hash] the webhook payload
68
+ # @param metadata [Hash, nil] optional metadata key-value pairs
69
+ # @return [Hash] response with "eventTypeId", "deliveryIds", and "status" keys
70
+ # @raise [APIError] on API error responses
71
+ def trigger(event_type, payload:, metadata: nil)
72
+ body = { "payload" => payload }
73
+ body["metadata"] = metadata if metadata
74
+
75
+ @http.request(
76
+ method: :post,
77
+ path: "/api/ingest/event/#{CGI.escape(event_type)}",
78
+ body: body
79
+ )
80
+ end
81
+
82
+ # Batch send to multiple specific endpoints (max 20 items).
83
+ #
84
+ # @param items [Array<Hash>] list of items, each with :endpoint_id, :payload, and optional :idempotency_key
85
+ # @return [Hash] response with "items" key containing per-item results
86
+ # @raise [APIError] on API error responses
87
+ #
88
+ # @example
89
+ # client.send_batch([
90
+ # { endpoint_id: "ep_abc", payload: { order: 1 } },
91
+ # { endpoint_id: "ep_def", payload: { order: 2 }, idempotency_key: "key-2" }
92
+ # ])
93
+ def send_batch(items)
94
+ mapped = items.map do |item|
95
+ entry = {
96
+ "endpointId" => item[:endpoint_id] || item["endpoint_id"],
97
+ "payload" => item[:payload] || item["payload"]
98
+ }
99
+ key = item[:idempotency_key] || item["idempotency_key"]
100
+ entry["idempotencyKey"] = key if key
101
+ entry
102
+ end
103
+
104
+ @http.request(
105
+ method: :post,
106
+ path: "/api/ingest/batch",
107
+ body: { "items" => mapped }
108
+ )
109
+ end
110
+
111
+ # Batch fan-out by event types (max 20 items).
112
+ #
113
+ # @param items [Array<Hash>] list of items, each with :event_type, :payload, and optional :metadata
114
+ # @return [Hash] response with "items" key containing per-item results
115
+ # @raise [APIError] on API error responses
116
+ #
117
+ # @example
118
+ # client.trigger_batch([
119
+ # { event_type: "order.paid", payload: { order_id: "123" } },
120
+ # { event_type: "user.created", payload: { user_id: "456" } }
121
+ # ])
122
+ def trigger_batch(items)
123
+ mapped = items.map do |item|
124
+ entry = {
125
+ "eventType" => item[:event_type] || item["event_type"],
126
+ "payload" => item[:payload] || item["payload"]
127
+ }
128
+ meta = item[:metadata] || item["metadata"]
129
+ entry["metadata"] = meta if meta
130
+ entry
131
+ end
132
+
133
+ @http.request(
134
+ method: :post,
135
+ path: "/api/ingest/event/batch",
136
+ body: { "items" => mapped }
137
+ )
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nahook
4
+ # Base error for all Nahook SDK errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the Nahook API returns an error response (4xx/5xx).
8
+ #
9
+ # @example Handling an API error
10
+ # begin
11
+ # client.send("ep_123", payload: { order: 1 })
12
+ # rescue Nahook::APIError => e
13
+ # puts e.status # => 404
14
+ # puts e.code # => "not_found"
15
+ # puts e.retryable? # => false
16
+ # end
17
+ class APIError < Error
18
+ # @return [Integer] HTTP status code
19
+ attr_reader :status
20
+
21
+ # @return [String] machine-readable error code from the API
22
+ attr_reader :code
23
+
24
+ # @return [Integer, nil] seconds the client should wait before retrying
25
+ attr_reader :retry_after
26
+
27
+ # @param status [Integer] HTTP status code
28
+ # @param code [String] machine-readable error code
29
+ # @param message [String] human-readable error message
30
+ # @param retry_after [Integer, nil] Retry-After header value in seconds
31
+ def initialize(status, code, message, retry_after = nil)
32
+ @status = status
33
+ @code = code
34
+ @retry_after = retry_after
35
+ super(message)
36
+ end
37
+
38
+ # Whether this error is safe to retry (5xx or 429).
39
+ #
40
+ # @return [Boolean]
41
+ def retryable?
42
+ status >= 500 || status == 429
43
+ end
44
+
45
+ # Whether this is an authentication or authorization error.
46
+ #
47
+ # @return [Boolean]
48
+ def auth_error?
49
+ status == 401 || (status == 403 && code == "token_disabled")
50
+ end
51
+
52
+ # Whether the requested resource was not found.
53
+ #
54
+ # @return [Boolean]
55
+ def not_found?
56
+ status == 404
57
+ end
58
+
59
+ # Whether the request was rate limited.
60
+ #
61
+ # @return [Boolean]
62
+ def rate_limited?
63
+ status == 429
64
+ end
65
+
66
+ # Whether the request failed validation.
67
+ #
68
+ # @return [Boolean]
69
+ def validation_error?
70
+ status == 400
71
+ end
72
+ end
73
+
74
+ # Raised when a network-level failure occurs (no HTTP response received).
75
+ class NetworkError < Error
76
+ # @return [Exception] the underlying error that caused this failure
77
+ attr_reader :original_error
78
+
79
+ # @param original_error [Exception] the original exception
80
+ def initialize(original_error)
81
+ @original_error = original_error
82
+ super("Network error: #{original_error.message}")
83
+ end
84
+ end
85
+
86
+ # Raised when a request exceeds the configured timeout.
87
+ class TimeoutError < Error
88
+ # @return [Integer] the timeout in milliseconds
89
+ attr_reader :timeout_ms
90
+
91
+ # @param timeout_ms [Integer] the configured timeout in milliseconds
92
+ def initialize(timeout_ms)
93
+ @timeout_ms = timeout_ms
94
+ super("Request timed out after #{timeout_ms}ms")
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "cgi"
6
+
7
+ module Nahook
8
+ # Low-level HTTP client used by both {Client} and {Management}.
9
+ #
10
+ # Handles request execution, retry logic with exponential backoff,
11
+ # and error parsing. Not intended for direct use.
12
+ #
13
+ # @api private
14
+ class HttpClient
15
+ DEFAULT_BASE_URL = "https://api.nahook.com"
16
+ DEFAULT_TIMEOUT_MS = 30_000
17
+ BASE_DELAY_MS = 500
18
+ MAX_DELAY_MS = 10_000
19
+
20
+ REGION_BASE_URLS = {
21
+ "us" => "https://us.api.nahook.com",
22
+ "eu" => "https://eu.api.nahook.com",
23
+ "ap" => "https://ap.api.nahook.com",
24
+ }.freeze
25
+
26
+ # @api private
27
+ def self.resolve_base_url(token)
28
+ if (m = token.match(/\Anhk_([a-z]{2})_/))
29
+ REGION_BASE_URLS[m[1]] || DEFAULT_BASE_URL
30
+ else
31
+ DEFAULT_BASE_URL
32
+ end
33
+ end
34
+
35
+ # @param token [String] bearer token for authentication
36
+ # @param base_url [String] API base URL
37
+ # @param timeout_ms [Integer] request timeout in milliseconds (default: 30000)
38
+ # @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
49
+ end
50
+ end
51
+
52
+ # Execute an HTTP request with optional retry logic.
53
+ #
54
+ # @param method [Symbol] HTTP method (:get, :post, :patch, :delete)
55
+ # @param path [String] request path (will be appended to base URL)
56
+ # @param body [Hash, nil] request body (will be JSON-encoded)
57
+ # @param query [Hash, nil] query parameters
58
+ # @return [Hash, nil] parsed JSON response, or nil for 204
59
+ # @raise [APIError] on 4xx/5xx responses
60
+ # @raise [NetworkError] on connection failures
61
+ # @raise [TimeoutError] on request timeout
62
+ def request(method:, path:, body: nil, query: nil)
63
+ execute_with_retry(method, path, body, query)
64
+ end
65
+
66
+ private
67
+
68
+ def execute_with_retry(method, path, body, query)
69
+ last_error = nil
70
+
71
+ (0..@retries).each do |attempt|
72
+ if attempt > 0
73
+ retry_after_ms = last_error.is_a?(APIError) ? (last_error.retry_after || 0) * 1000 : nil
74
+ delay = calculate_delay(attempt - 1, retry_after_ms)
75
+ sleep(delay / 1000.0)
76
+ end
77
+
78
+ begin
79
+ response = perform_request(method, path, body, query)
80
+
81
+ unless response.success?
82
+ error = parse_error(response)
83
+ if attempt < @retries && retryable?(error)
84
+ last_error = error
85
+ next
86
+ end
87
+ raise error
88
+ end
89
+
90
+ return nil if response.status == 204
91
+ return JSON.parse(response.body)
92
+
93
+ rescue APIError
94
+ raise
95
+
96
+ rescue Faraday::TimeoutError => e
97
+ last_error = TimeoutError.new(@timeout_ms)
98
+ raise last_error unless attempt < @retries && retryable?(last_error)
99
+
100
+ rescue Faraday::ConnectionFailed => e
101
+ last_error = NetworkError.new(e)
102
+ raise last_error unless attempt < @retries && retryable?(last_error)
103
+ end
104
+ end
105
+
106
+ raise last_error
107
+ end
108
+
109
+ def perform_request(method, path, body, query)
110
+ @conn.run_request(method, path, nil, request_headers(body)) do |req|
111
+ req.body = JSON.generate(body) if body
112
+ if query
113
+ query.each do |key, value|
114
+ req.params[key.to_s] = value.to_s unless value.nil?
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ def request_headers(body)
121
+ headers = {
122
+ "Authorization" => "Bearer #{@token}",
123
+ "Accept" => "application/json",
124
+ "User-Agent" => "nahook-ruby/#{Nahook::VERSION}"
125
+ }
126
+ headers["Content-Type"] = "application/json" if body
127
+ headers
128
+ end
129
+
130
+ def parse_error(response)
131
+ retry_after = response.headers["retry-after"]
132
+ retry_after_secs = retry_after ? retry_after.to_i : nil
133
+
134
+ begin
135
+ body = JSON.parse(response.body)
136
+ code = body.dig("error", "code") || "unknown"
137
+ message = body.dig("error", "message") || response.reason_phrase || "Unknown error"
138
+ rescue JSON::ParserError
139
+ code = "unknown"
140
+ message = response.reason_phrase || "Unknown error"
141
+ end
142
+
143
+ APIError.new(response.status, code, message, retry_after_secs)
144
+ end
145
+
146
+ def calculate_delay(attempt, retry_after_ms = nil)
147
+ if retry_after_ms && retry_after_ms > 0
148
+ return retry_after_ms
149
+ end
150
+
151
+ exponential = [MAX_DELAY_MS, BASE_DELAY_MS * (2**attempt)].min
152
+ exponential * rand
153
+ end
154
+
155
+ def retryable?(error)
156
+ case error
157
+ when APIError then error.retryable?
158
+ when NetworkError then true
159
+ when TimeoutError then true
160
+ else false
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nahook
4
+ # Client for the Nahook Management API.
5
+ #
6
+ # Provides programmatic access to manage workspaces, endpoints, event types,
7
+ # applications, subscriptions, and portal sessions. Intended for server-side
8
+ # use with a management token.
9
+ #
10
+ # Unlike {Client}, the Management client does not support retries --
11
+ # management operations are not idempotent by default.
12
+ #
13
+ # @example
14
+ # mgmt = Nahook::Management.new("nhm_your_token")
15
+ #
16
+ # # List endpoints
17
+ # result = mgmt.endpoints.list("ws_abc123")
18
+ # result["data"].each { |ep| puts ep["url"] }
19
+ #
20
+ # # Create an endpoint
21
+ # endpoint = mgmt.endpoints.create("ws_abc123",
22
+ # url: "https://example.com/webhook",
23
+ # description: "Production webhook"
24
+ # )
25
+ class Management
26
+ # @return [Resources::Endpoints]
27
+ attr_reader :endpoints
28
+
29
+ # @return [Resources::EventTypes]
30
+ attr_reader :event_types
31
+
32
+ # @return [Resources::Applications]
33
+ attr_reader :applications
34
+
35
+ # @return [Resources::Subscriptions]
36
+ attr_reader :subscriptions
37
+
38
+ # @return [Resources::PortalSessions]
39
+ attr_reader :portal_sessions
40
+
41
+ # @return [Resources::Environments]
42
+ attr_reader :environments
43
+
44
+ # @param token [String] management token (must start with "nhm_")
45
+ # @param base_url [String] API base URL
46
+ # @param timeout_ms [Integer] request timeout in milliseconds (default: 30000)
47
+ # @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)
49
+ unless token.start_with?("nhm_")
50
+ raise ArgumentError, "Invalid management token: must start with 'nhm_'"
51
+ end
52
+
53
+ http = HttpClient.new(token: token, base_url: base_url, timeout_ms: timeout_ms)
54
+
55
+ @endpoints = Resources::Endpoints.new(http)
56
+ @event_types = Resources::EventTypes.new(http)
57
+ @applications = Resources::Applications.new(http)
58
+ @subscriptions = Resources::Subscriptions.new(http)
59
+ @portal_sessions = Resources::PortalSessions.new(http)
60
+ @environments = Resources::Environments.new(http)
61
+ end
62
+ end
63
+ end