hatchet-sdk 0.0.0 → 0.0.1

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: 04cba5845adc6fdaa4fe8211dcf3326ff8e029d458ea80db7c9aa8018092a466
4
- data.tar.gz: 54e7411d0da2caba1198049225642b1242fe25838d62d1b70560fefdd9bc30f5
3
+ metadata.gz: 415716e0955c26837a2887bd5703497b2bda92f167593742747aff057d4db77a
4
+ data.tar.gz: 510bc99100f79cb590eafc874b0e4114035bec15cff11f77229a6b59a497ad70
5
5
  SHA512:
6
- metadata.gz: cd5c9791787389f7a844f2cb61f9394b31ee8212a8fde4a06701be212683536de20cf78fa5db8c55a8ab03fcab09c190101ef6dd084720a92b66f739d0f2016d
7
- data.tar.gz: 86d51a2da75ccd5cdae6c9fd01fb6e18e8956227661f34cd892ad26b517e51ce4add58b93fd212eec92da7621a7c4aa729746ebe39804d79dd8d5ed278f0c62c
6
+ metadata.gz: 174e40cbb3d3c5470c3ae2aaa3a148048240fd4665519177e33d2dddd4f567c3105ba01dad812eefa586fda560c353395689a3f0f228e19c9a554a670d2508df
7
+ data.tar.gz: ae343d08ea3648c703a7d4f66b7bd23b0527dca64ee6beb0a4fee66c300c8ac95d42ed50f30155a5372cb6ebf0bd663c7c4fa8776c13a09b31250c56e8863958
data/CLAUDE.md CHANGED
@@ -4,7 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project Overview
6
6
 
7
- This is the Ruby SDK for Hatchet (gem name: hatchet-sdk), currently in early development (version 0.0.0). The project follows standard Ruby gem conventions with a simple structure containing a main `Hatchet` module with a `Client` class for API key authentication.
7
+ This is the Ruby SDK for Hatchet (gem name: hatchet-sdk), currently in early development (version 0.0.0). The project follows standard Ruby gem conventions with a simple structure containing a main `Hatchet` module with a `Client` class for JWT token authentication.
8
+
9
+ The SDK includes comprehensive documentation and type signatures for excellent IDE support, including parameter hints, auto-completion, and type checking.
8
10
 
9
11
  ## Common Commands
10
12
 
@@ -25,18 +27,53 @@ This is the Ruby SDK for Hatchet (gem name: hatchet-sdk), currently in early dev
25
27
  **Gem Management:**
26
28
  - `bundle exec rake release` - Release new version (updates version, creates git tag, pushes to rubygems)
27
29
 
30
+ **Documentation:**
31
+ - `yard doc` - Generate YARD documentation (if yard gem is installed)
32
+ - `rbs validate` - Validate RBS type signatures for syntax errors
33
+
28
34
  ## Architecture
29
35
 
30
36
  **Core Structure:**
31
37
  - `lib/hatchet-sdk.rb` - Main entry point, defines `Hatchet` module with `Error` and `Client` classes
32
38
  - `lib/hatchet/version.rb` - Version constant
33
- - `spec/` - RSpec tests with monkey patching disabled
34
- - `sig/hatchet-sdk.rbs` - Ruby type signatures
39
+ - `lib/hatchet/config.rb` - Configuration classes with comprehensive JWT token support
40
+ - `spec/` - RSpec tests with monkey patching disabled (36+ test cases)
41
+ - `sig/hatchet-sdk.rbs` - Ruby type signatures for IDE integration
35
42
 
36
43
  **Key Classes:**
37
- - `Hatchet::Client` - Main client class that accepts an API key for authentication
44
+ - `Hatchet::Client` - Main client class that accepts JWT token for authentication
45
+ - `Hatchet::Config` - Configuration class supporting multiple sources (params, env vars, JWT payload)
46
+ - `Hatchet::TLSConfig` - TLS configuration for secure connections
47
+ - `Hatchet::HealthcheckConfig` - Worker health monitoring configuration
48
+ - `Hatchet::OpenTelemetryConfig` - Observability configuration
38
49
  - `Hatchet::Error` - Base error class for gem-specific exceptions
39
50
 
51
+ **Configuration Sources (priority order):**
52
+ 1. Explicit constructor parameters (highest priority)
53
+ 2. Environment variables (`HATCHET_CLIENT_*`)
54
+ 3. JWT token payload (tenant_id extracted from 'sub' field)
55
+ 4. Default values (lowest priority)
56
+
57
+ **Documentation & IDE Support:**
58
+ - **YARD documentation** - Comprehensive JSDoc-style comments with examples
59
+ - **RBS type signatures** - Complete type definitions for IDE parameter hints
60
+ - **Sorbet compatibility** - Tagged with `# typed: strict` for type checking
61
+ - IDEs with Ruby LSP/RubyMine will show parameter hints, auto-completion, and types
62
+
40
63
  The codebase uses frozen string literals and follows Ruby 3.1+ requirements.
41
64
 
65
+ ## Development Notes
66
+
67
+ **When adding new configuration options:**
68
+ 1. Add the parameter to `Config#initialize` method
69
+ 2. Update the `@option` YARD documentation in both `Client` and `Config` classes
70
+ 3. Add the parameter to RBS type signatures in `sig/hatchet-sdk.rbs`
71
+ 4. Add comprehensive test coverage in `spec/hatchet/config_spec.rb`
72
+ 5. Update this CLAUDE.md file with any architectural changes
73
+
74
+ **Testing JWT token functionality:**
75
+ - Use `Base64.encode64('{"sub":"tenant-id"}').gsub(/\n/, "").gsub(/=+$/, "")` to create test JWT payloads
76
+ - The config extracts tenant_id from the 'sub' field in JWT tokens
77
+ - Test both explicit tenant_id override and JWT extraction scenarios
78
+
42
79
  Keep the CLAUDE.md instructions up to date as the project continues to develop.
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "json"
5
+ require "base64"
6
+
7
+ module Hatchet
8
+ # TLS configuration for client connections
9
+ #
10
+ # @example Basic TLS setup
11
+ # tls = TLSConfig.new(strategy: "tls", server_name: "api.hatchet.com")
12
+ #
13
+ # @example Custom certificate setup
14
+ # tls = TLSConfig.new(
15
+ # strategy: "mtls",
16
+ # cert_file: "/path/to/client.crt",
17
+ # key_file: "/path/to/client.key",
18
+ # root_ca_file: "/path/to/ca.crt"
19
+ # )
20
+ class TLSConfig
21
+ # @!attribute strategy
22
+ # @return [String] TLS strategy ("tls", "mtls", "insecure")
23
+ # @!attribute cert_file
24
+ # @return [String, nil] Path to client certificate file for mTLS
25
+ # @!attribute key_file
26
+ # @return [String, nil] Path to client private key file for mTLS
27
+ # @!attribute root_ca_file
28
+ # @return [String, nil] Path to root CA certificate file
29
+ # @!attribute server_name
30
+ # @return [String] Server name for TLS verification
31
+ attr_accessor :strategy, :cert_file, :key_file, :root_ca_file, :server_name
32
+
33
+ # Initialize TLS configuration
34
+ #
35
+ # @param options [Hash] TLS configuration options
36
+ # @option options [String] :strategy TLS strategy (default: "tls")
37
+ # @option options [String] :cert_file Path to client certificate file
38
+ # @option options [String] :key_file Path to client private key file
39
+ # @option options [String] :root_ca_file Path to root CA certificate file
40
+ # @option options [String] :server_name Server name for TLS verification
41
+ def initialize(**options)
42
+ @strategy = options[:strategy] || env_var("HATCHET_CLIENT_TLS_STRATEGY") || "tls"
43
+ @cert_file = options[:cert_file] || env_var("HATCHET_CLIENT_TLS_CERT_FILE")
44
+ @key_file = options[:key_file] || env_var("HATCHET_CLIENT_TLS_KEY_FILE")
45
+ @root_ca_file = options[:root_ca_file] || env_var("HATCHET_CLIENT_TLS_ROOT_CA_FILE")
46
+ @server_name = options[:server_name] || env_var("HATCHET_CLIENT_TLS_SERVER_NAME") || ""
47
+ end
48
+
49
+ private
50
+
51
+ def env_var(name)
52
+ ENV[name]
53
+ end
54
+ end
55
+
56
+ # Healthcheck configuration for worker health monitoring
57
+ #
58
+ # @example Enable healthcheck on custom port
59
+ # healthcheck = HealthcheckConfig.new(enabled: true, port: 8080)
60
+ class HealthcheckConfig
61
+ # @!attribute port
62
+ # @return [Integer] Port number for healthcheck endpoint
63
+ # @!attribute enabled
64
+ # @return [Boolean] Whether healthcheck is enabled
65
+ attr_accessor :port, :enabled
66
+
67
+ # Initialize healthcheck configuration
68
+ #
69
+ # @param options [Hash] Healthcheck configuration options
70
+ # @option options [Integer] :port Port number for healthcheck endpoint (default: 8001)
71
+ # @option options [Boolean] :enabled Whether healthcheck is enabled (default: false)
72
+ def initialize(**options)
73
+ @port = parse_int(options[:port] || env_var("HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT")) || 8001
74
+ @enabled = parse_bool(options[:enabled] || env_var("HATCHET_CLIENT_WORKER_HEALTHCHECK_ENABLED")) || false
75
+ end
76
+
77
+ private
78
+
79
+ def env_var(name)
80
+ ENV[name]
81
+ end
82
+
83
+ def parse_int(value)
84
+ return nil if value.nil?
85
+ return value if value.is_a?(Integer)
86
+ return nil if value.respond_to?(:empty?) && value.empty?
87
+
88
+ Integer(value)
89
+ rescue ArgumentError
90
+ nil
91
+ end
92
+
93
+ def parse_bool(value)
94
+ return nil if value.nil?
95
+ return value if [true, false].include?(value)
96
+
97
+ %w[true 1 yes on].include?(value.to_s.downcase)
98
+ end
99
+ end
100
+
101
+ # OpenTelemetry configuration for observability
102
+ #
103
+ # @example Exclude sensitive attributes from telemetry
104
+ # otel = OpenTelemetryConfig.new(excluded_attributes: ["password", "api_key"])
105
+ class OpenTelemetryConfig
106
+ # @!attribute excluded_attributes
107
+ # @return [Array<String>] List of attribute names to exclude from telemetry
108
+ attr_accessor :excluded_attributes
109
+
110
+ # Initialize OpenTelemetry configuration
111
+ #
112
+ # @param options [Hash] OpenTelemetry configuration options
113
+ # @option options [Array<String>] :excluded_attributes List of attribute names to exclude from telemetry
114
+ def initialize(**options)
115
+ @excluded_attributes = options[:excluded_attributes] ||
116
+ parse_json_array(
117
+ env_var("HATCHET_CLIENT_OPENTELEMETRY_EXCLUDED_ATTRIBUTES")
118
+ ) || []
119
+ end
120
+
121
+ private
122
+
123
+ def env_var(name)
124
+ ENV[name]
125
+ end
126
+
127
+ def parse_json_array(value)
128
+ return nil if value.nil? || value.empty?
129
+
130
+ JSON.parse(value)
131
+ rescue JSON::ParserError
132
+ nil
133
+ end
134
+ end
135
+
136
+ # Configuration class for Hatchet client settings
137
+ #
138
+ # This class manages all configuration options for the Hatchet client, including
139
+ # token authentication, connection settings, and various client behaviors.
140
+ # Configuration can be set via constructor options, environment variables, or
141
+ # extracted from JWT tokens.
142
+ #
143
+ # @example Basic configuration
144
+ # config = Config.new(token: "your-jwt-token")
145
+ #
146
+ # @example Full configuration with all options
147
+ # config = Config.new(
148
+ # token: "your-jwt-token",
149
+ # host_port: "localhost:7070",
150
+ # server_url: "https://api.hatchet.com",
151
+ # namespace: "production",
152
+ # tenant_id: "custom-tenant",
153
+ # worker_preset_labels: { "env" => "prod", "region" => "us-east-1" }
154
+ # )
155
+ #
156
+ # @example Configuration via environment variables
157
+ # ENV["HATCHET_CLIENT_TOKEN"] = "your-jwt-token"
158
+ # ENV["HATCHET_CLIENT_HOST_PORT"] = "localhost:7070"
159
+ # config = Config.new # Will load from environment
160
+ class Config
161
+ DEFAULT_HOST_PORT = "localhost:7070"
162
+ DEFAULT_SERVER_URL = "https://app.dev.hatchet-tools.com"
163
+ DEFAULT_GRPC_MAX_MESSAGE_LENGTH = 4 * 1024 * 1024 # 4MB
164
+
165
+ ENV_FILE_NAMES = [".env", ".env.hatchet", ".env.dev", ".env.local"].freeze
166
+
167
+ # @!attribute token
168
+ # @return [String] JWT token for authentication
169
+ # @!attribute tenant_id
170
+ # @return [String] Tenant ID (extracted from JWT 'sub' field if not provided)
171
+ # @!attribute host_port
172
+ # @return [String] gRPC server host and port
173
+ # @!attribute server_url
174
+ # @return [String] Server URL for HTTP requests
175
+ # @!attribute namespace
176
+ # @return [String] Namespace prefix for resource names
177
+ # @!attribute logger
178
+ # @return [Logger] Logger instance
179
+ # @!attribute listener_v2_timeout
180
+ # @return [Integer, nil] Timeout for listener v2 in milliseconds
181
+ # @!attribute grpc_max_recv_message_length
182
+ # @return [Integer] Maximum gRPC receive message length in bytes
183
+ # @!attribute grpc_max_send_message_length
184
+ # @return [Integer] Maximum gRPC send message length in bytes
185
+ # @!attribute worker_preset_labels
186
+ # @return [Hash<String, String>] Hash of preset labels for workers
187
+ # @!attribute enable_force_kill_sync_threads
188
+ # @return [Boolean] Enable force killing of sync threads
189
+ # @!attribute enable_thread_pool_monitoring
190
+ # @return [Boolean] Enable thread pool monitoring
191
+ # @!attribute terminate_worker_after_num_tasks
192
+ # @return [Integer, nil] Terminate worker after this many tasks
193
+ # @!attribute disable_log_capture
194
+ # @return [Boolean] Disable log capture
195
+ # @!attribute grpc_enable_fork_support
196
+ # @return [Boolean] Enable gRPC fork support
197
+ # @!attribute tls_config
198
+ # @return [TLSConfig] TLS configuration
199
+ # @!attribute healthcheck
200
+ # @return [HealthcheckConfig] Healthcheck configuration
201
+ # @!attribute otel
202
+ # @return [OpenTelemetryConfig] OpenTelemetry configuration
203
+ attr_accessor :token, :tenant_id, :host_port, :server_url, :namespace,
204
+ :logger, :listener_v2_timeout, :grpc_max_recv_message_length,
205
+ :grpc_max_send_message_length, :worker_preset_labels,
206
+ :enable_force_kill_sync_threads, :enable_thread_pool_monitoring,
207
+ :terminate_worker_after_num_tasks, :disable_log_capture,
208
+ :grpc_enable_fork_support, :tls_config, :healthcheck, :otel
209
+
210
+ # Initialize a new configuration instance
211
+ #
212
+ # Configuration values are loaded in the following priority order:
213
+ # 1. Explicit constructor options (highest priority)
214
+ # 2. Environment variables (HATCHET_CLIENT_*)
215
+ # 3. JWT token payload (for tenant_id from 'sub' field)
216
+ # 4. Default values (lowest priority)
217
+ #
218
+ # @param options [Hash] Configuration options
219
+ # @option options [String] :token JWT token for authentication (required)
220
+ # @option options [String] :tenant_id Override tenant ID (extracted from JWT 'sub' field if not provided)
221
+ # @option options [String] :host_port gRPC server host and port (default: "localhost:7070")
222
+ # @option options [String] :server_url Server URL for HTTP requests (default: "https://app.dev.hatchet-tools.com")
223
+ # @option options [String] :namespace Namespace prefix for resource names (default: "")
224
+ # @option options [Logger] :logger Custom logger instance (default: Logger.new($stdout))
225
+ # @option options [Integer] :listener_v2_timeout Timeout for listener v2 in milliseconds
226
+ # @option options [Integer] :grpc_max_recv_message_length Maximum gRPC receive message length in bytes (default: 4MB)
227
+ # @option options [Integer] :grpc_max_send_message_length Maximum gRPC send message length in bytes (default: 4MB)
228
+ # @option options [Hash<String, String>] :worker_preset_labels Hash of preset labels for workers
229
+ # @option options [Boolean] :enable_force_kill_sync_threads Enable force killing of sync threads (default: false)
230
+ # @option options [Boolean] :enable_thread_pool_monitoring Enable thread pool monitoring (default: false)
231
+ # @option options [Integer] :terminate_worker_after_num_tasks Terminate worker after this many tasks
232
+ # @option options [Boolean] :disable_log_capture Disable log capture (default: false)
233
+ # @option options [Boolean] :grpc_enable_fork_support Enable gRPC fork support (default: false)
234
+ # @option options [TLSConfig] :tls_config Custom TLS configuration
235
+ # @option options [HealthcheckConfig] :healthcheck Custom healthcheck configuration
236
+ # @option options [OpenTelemetryConfig] :otel Custom OpenTelemetry configuration
237
+ #
238
+ # @raise [Error] if token is missing, empty, or not a valid JWT
239
+ def initialize(**options)
240
+ load_env_files
241
+ @explicitly_set = options.keys.to_set
242
+
243
+ @token = options[:token] || env_var("HATCHET_CLIENT_TOKEN") || ""
244
+ @tenant_id = options[:tenant_id] || env_var("HATCHET_CLIENT_TENANT_ID") || ""
245
+ @host_port = options[:host_port] || env_var("HATCHET_CLIENT_HOST_PORT") || DEFAULT_HOST_PORT
246
+ @server_url = options[:server_url] || env_var("HATCHET_CLIENT_SERVER_URL") || DEFAULT_SERVER_URL
247
+ @namespace = options[:namespace] || env_var("HATCHET_CLIENT_NAMESPACE") || ""
248
+ @logger = options[:logger] || Logger.new($stdout)
249
+
250
+ @listener_v2_timeout = parse_int(options[:listener_v2_timeout] || env_var("HATCHET_CLIENT_LISTENER_V2_TIMEOUT"))
251
+ @grpc_max_recv_message_length = parse_int(
252
+ options[:grpc_max_recv_message_length] ||
253
+ env_var("HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH")
254
+ ) || DEFAULT_GRPC_MAX_MESSAGE_LENGTH
255
+ @grpc_max_send_message_length = parse_int(
256
+ options[:grpc_max_send_message_length] ||
257
+ env_var("HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH")
258
+ ) || DEFAULT_GRPC_MAX_MESSAGE_LENGTH
259
+
260
+ @worker_preset_labels = options[:worker_preset_labels] ||
261
+ parse_hash(env_var("HATCHET_CLIENT_WORKER_PRESET_LABELS")) || {}
262
+ @enable_force_kill_sync_threads = parse_bool(
263
+ options[:enable_force_kill_sync_threads] ||
264
+ env_var("HATCHET_CLIENT_ENABLE_FORCE_KILL_SYNC_THREADS")
265
+ ) || false
266
+ @enable_thread_pool_monitoring = parse_bool(
267
+ options[:enable_thread_pool_monitoring] ||
268
+ env_var("HATCHET_CLIENT_ENABLE_THREAD_POOL_MONITORING")
269
+ ) || false
270
+ @terminate_worker_after_num_tasks = parse_int(
271
+ options[:terminate_worker_after_num_tasks] ||
272
+ env_var("HATCHET_CLIENT_TERMINATE_WORKER_AFTER_NUM_TASKS")
273
+ )
274
+ @disable_log_capture = parse_bool(
275
+ options[:disable_log_capture] ||
276
+ env_var("HATCHET_CLIENT_DISABLE_LOG_CAPTURE")
277
+ ) || false
278
+ @grpc_enable_fork_support = parse_bool(
279
+ options[:grpc_enable_fork_support] ||
280
+ env_var("HATCHET_CLIENT_GRPC_ENABLE_FORK_SUPPORT")
281
+ ) || false
282
+
283
+ # Initialize nested configurations
284
+ @tls_config = options[:tls_config] || TLSConfig.new
285
+ @healthcheck = options[:healthcheck] || HealthcheckConfig.new
286
+ @otel = options[:otel] || OpenTelemetryConfig.new
287
+
288
+ validate!
289
+ apply_token_defaults if valid_jwt_token?
290
+ apply_address_defaults if valid_jwt_token?
291
+ normalize_namespace!
292
+ end
293
+
294
+ def validate!
295
+ raise Error, "Hatchet Token is required. Please set HATCHET_CLIENT_TOKEN in your environment." if token.nil? || token.empty?
296
+
297
+ return if valid_jwt_token?
298
+
299
+ raise Error,
300
+ "Hatchet Token must be a valid JWT."
301
+ end
302
+
303
+ # Apply namespace prefix to a resource name
304
+ #
305
+ # @param resource_name [String, nil] The resource name to namespace
306
+ # @param namespace_override [String, nil] Optional namespace to use instead of the configured one
307
+ # @return [String, nil] The namespaced resource name, or nil if resource_name is nil
308
+ #
309
+ # @example Apply default namespace
310
+ # config = Config.new(token: "token", namespace: "prod")
311
+ # config.apply_namespace("workflow") #=> "prod_workflow"
312
+ #
313
+ # @example Apply custom namespace
314
+ # config.apply_namespace("workflow", namespace_override: "staging_") #=> "staging_workflow"
315
+ #
316
+ # @example Skip namespace if already present
317
+ # config.apply_namespace("prod_workflow") #=> "prod_workflow"
318
+ def apply_namespace(resource_name, namespace_override: nil)
319
+ return resource_name if resource_name.nil?
320
+
321
+ namespace_to_use = namespace_override || namespace
322
+ return resource_name if namespace_to_use.empty?
323
+ return resource_name if resource_name.start_with?(namespace_to_use)
324
+
325
+ "#{namespace_to_use}#{resource_name}"
326
+ end
327
+
328
+ def hash
329
+ to_h.hash
330
+ end
331
+
332
+ # Convert configuration to a hash representation
333
+ #
334
+ # @return [Hash<Symbol, Object>] Hash containing all configuration values
335
+ def to_h
336
+ {
337
+ token: token,
338
+ tenant_id: tenant_id,
339
+ host_port: host_port,
340
+ server_url: server_url,
341
+ namespace: namespace,
342
+ listener_v2_timeout: listener_v2_timeout,
343
+ grpc_max_recv_message_length: grpc_max_recv_message_length,
344
+ grpc_max_send_message_length: grpc_max_send_message_length,
345
+ worker_preset_labels: worker_preset_labels,
346
+ enable_force_kill_sync_threads: enable_force_kill_sync_threads,
347
+ enable_thread_pool_monitoring: enable_thread_pool_monitoring,
348
+ terminate_worker_after_num_tasks: terminate_worker_after_num_tasks,
349
+ disable_log_capture: disable_log_capture,
350
+ grpc_enable_fork_support: grpc_enable_fork_support
351
+ }
352
+ end
353
+
354
+ private
355
+
356
+ def valid_jwt_token?
357
+ !token.nil? && !token.empty? && token.start_with?("ey")
358
+ end
359
+
360
+ def apply_token_defaults
361
+ return unless tenant_id.empty?
362
+
363
+ extracted_tenant_id = extract_tenant_id_from_jwt
364
+ @tenant_id = extracted_tenant_id || ""
365
+ end
366
+
367
+ def apply_address_defaults
368
+ jwt_server_url, jwt_host_port = extract_addresses_from_jwt
369
+
370
+ @host_port = jwt_host_port if jwt_host_port && !explicitly_set?(:host_port)
371
+ @server_url = jwt_server_url if jwt_server_url && !explicitly_set?(:server_url)
372
+
373
+ # Set TLS server name if not already set
374
+ return unless tls_config.server_name.empty?
375
+
376
+ tls_config.server_name = host_port.split(":").first || "localhost"
377
+ end
378
+
379
+ def normalize_namespace!
380
+ return if namespace.empty?
381
+
382
+ @namespace = namespace.downcase
383
+ @namespace += "_" unless @namespace.end_with?("_")
384
+ end
385
+
386
+ def load_env_files
387
+ ENV_FILE_NAMES.each do |env_file|
388
+ next unless File.exist?(env_file)
389
+
390
+ File.foreach(env_file) do |line|
391
+ line = line.strip
392
+ next if line.empty? || line.start_with?("#")
393
+
394
+ key, value = line.split("=", 2)
395
+ next unless key && value
396
+
397
+ ENV[key] ||= value.gsub(/\A['"]|['"]\z/, "") # Remove surrounding quotes
398
+ end
399
+ end
400
+ end
401
+
402
+ def env_var(name)
403
+ ENV[name]
404
+ end
405
+
406
+ def parse_int(value)
407
+ return nil if value.nil?
408
+ return value if value.is_a?(Integer)
409
+ return nil if value.respond_to?(:empty?) && value.empty?
410
+
411
+ Integer(value)
412
+ rescue ArgumentError
413
+ nil
414
+ end
415
+
416
+ def parse_bool(value)
417
+ return nil if value.nil?
418
+ return value if [true, false].include?(value)
419
+
420
+ %w[true 1 yes on].include?(value.to_s.downcase)
421
+ end
422
+
423
+ def parse_hash(value)
424
+ return nil if value.nil? || value.empty?
425
+
426
+ # Simple key=value,key2=value2 parsing
427
+ result = {}
428
+ value.split(",").each do |pair|
429
+ key, val = pair.split("=", 2)
430
+ result[key.strip] = val&.strip if key && val
431
+ end
432
+ result
433
+ rescue StandardError
434
+ nil
435
+ end
436
+
437
+ def explicitly_set?(attr)
438
+ @explicitly_set.include?(attr)
439
+ end
440
+
441
+ def extract_tenant_id_from_jwt
442
+ return nil unless valid_jwt_token?
443
+
444
+ payload = decode_jwt_payload
445
+ payload&.dig("sub")
446
+ rescue StandardError
447
+ nil
448
+ end
449
+
450
+ def extract_addresses_from_jwt
451
+ return [nil, nil] unless valid_jwt_token?
452
+
453
+ payload = decode_jwt_payload
454
+ return [nil, nil] unless payload
455
+
456
+ server_url = payload["server_url"]
457
+ grpc_broadcast_address = payload["grpc_broadcast_address"]
458
+
459
+ [server_url, grpc_broadcast_address]
460
+ rescue StandardError
461
+ [nil, nil]
462
+ end
463
+
464
+ def decode_jwt_payload
465
+ return nil unless valid_jwt_token?
466
+
467
+ # JWT has three parts separated by dots: header.payload.signature
468
+ parts = token.split(".")
469
+ return nil unless parts.length == 3
470
+
471
+ # Decode the payload (second part)
472
+ payload_part = parts[1]
473
+ # Add padding if needed for Base64 decoding
474
+ payload_part += "=" * (4 - payload_part.length % 4) if payload_part.length % 4 != 0
475
+
476
+ decoded_payload = Base64.decode64(payload_part)
477
+ JSON.parse(decoded_payload)
478
+ rescue StandardError
479
+ nil
480
+ end
481
+ end
482
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hatchet
4
- VERSION = "0.0.0"
4
+ VERSION = "0.0.1"
5
5
  end
data/lib/hatchet-sdk.rb CHANGED
@@ -1,15 +1,65 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  require_relative "hatchet/version"
5
+ require_relative "hatchet/config"
4
6
 
7
+ # Ruby SDK for Hatchet workflow engine
8
+ #
9
+ # @see https://docs.hatchet.run for Hatchet documentation
5
10
  module Hatchet
11
+ # Base error class for all Hatchet-related errors
6
12
  class Error < StandardError; end
7
13
 
14
+ # The main client for interacting with Hatchet services.
15
+ #
16
+ # @example Basic usage with API token
17
+ # hatchet = Hatchet::Client.new()
18
+ #
19
+ # @example With custom configuration
20
+ # hatchet = Hatchet::Client.new(
21
+ # token: "your-jwt-token",
22
+ # namespace: "production"
23
+ # )
8
24
  class Client
9
- attr_reader :api_key
25
+ # @return [Config] The configuration object used by this client
26
+ attr_reader :config
10
27
 
11
- def initialize(api_key)
12
- @api_key = api_key
28
+ # Initialize a new Hatchet client with the given configuration options.
29
+ #
30
+ # @param options [Hash] Configuration options for the client
31
+ # @option options [String] :token The JWT token for authentication (required)
32
+ # @option options [String] :tenant_id Override tenant ID (extracted from JWT token 'sub' field if not provided)
33
+ # @option options [String] :host_port gRPC server host and port (default: "localhost:7070")
34
+ # @option options [String] :server_url Server URL for HTTP requests (default: "https://app.dev.hatchet-tools.com")
35
+ # @option options [String] :namespace Namespace prefix for resource names (default: "")
36
+ # @option options [Logger] :logger Custom logger instance (default: Logger.new($stdout))
37
+ # @option options [Integer] :listener_v2_timeout Timeout for listener v2 in milliseconds
38
+ # @option options [Integer] :grpc_max_recv_message_length Maximum gRPC receive message length (default: 4MB)
39
+ # @option options [Integer] :grpc_max_send_message_length Maximum gRPC send message length (default: 4MB)
40
+ # @option options [Hash] :worker_preset_labels Hash of preset labels for workers
41
+ # @option options [Boolean] :enable_force_kill_sync_threads Enable force killing of sync threads (default: false)
42
+ # @option options [Boolean] :enable_thread_pool_monitoring Enable thread pool monitoring (default: false)
43
+ # @option options [Integer] :terminate_worker_after_num_tasks Terminate worker after this many tasks
44
+ # @option options [Boolean] :disable_log_capture Disable log capture (default: false)
45
+ # @option options [Boolean] :grpc_enable_fork_support Enable gRPC fork support (default: false)
46
+ # @option options [TLSConfig] :tls_config Custom TLS configuration
47
+ # @option options [HealthcheckConfig] :healthcheck Custom healthcheck configuration
48
+ # @option options [OpenTelemetryConfig] :otel Custom OpenTelemetry configuration
49
+ #
50
+ # @raise [Error] if token or configuration is missing or invalid
51
+ #
52
+ # @example Initialize with minimal configuration
53
+ # client = Hatchet::Client.new()
54
+ #
55
+ # @example Initialize with custom options
56
+ # client = Hatchet::Client.new(
57
+ # token: "eyJhbGciOiJIUzI1NiJ9...",
58
+ # namespace: "my_app",
59
+ # worker_preset_labels: { "env" => "production", "version" => "1.0.0" }
60
+ # )
61
+ def initialize(**options)
62
+ @config = Config.new(**options)
13
63
  end
14
64
  end
15
65
  end
data/sig/hatchet-sdk.rbs CHANGED
@@ -1,4 +1,106 @@
1
1
  module Hatchet
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class Client
8
+ attr_reader config: Config
9
+
10
+ def initialize: (
11
+ ?token: String,
12
+ ?tenant_id: String,
13
+ ?host_port: String,
14
+ ?server_url: String,
15
+ ?namespace: String,
16
+ ?logger: Logger,
17
+ ?listener_v2_timeout: Integer,
18
+ ?grpc_max_recv_message_length: Integer,
19
+ ?grpc_max_send_message_length: Integer,
20
+ ?worker_preset_labels: Hash[String, String],
21
+ ?enable_force_kill_sync_threads: bool,
22
+ ?enable_thread_pool_monitoring: bool,
23
+ ?terminate_worker_after_num_tasks: Integer,
24
+ ?disable_log_capture: bool,
25
+ ?grpc_enable_fork_support: bool,
26
+ ?tls_config: TLSConfig,
27
+ ?healthcheck: HealthcheckConfig,
28
+ ?otel: OpenTelemetryConfig
29
+ ) -> void
30
+ end
31
+
32
+ class Config
33
+ attr_accessor token: String
34
+ attr_accessor tenant_id: String
35
+ attr_accessor host_port: String
36
+ attr_accessor server_url: String
37
+ attr_accessor namespace: String
38
+ attr_accessor logger: Logger
39
+ attr_accessor listener_v2_timeout: Integer?
40
+ attr_accessor grpc_max_recv_message_length: Integer
41
+ attr_accessor grpc_max_send_message_length: Integer
42
+ attr_accessor worker_preset_labels: Hash[String, String]
43
+ attr_accessor enable_force_kill_sync_threads: bool
44
+ attr_accessor enable_thread_pool_monitoring: bool
45
+ attr_accessor terminate_worker_after_num_tasks: Integer?
46
+ attr_accessor disable_log_capture: bool
47
+ attr_accessor grpc_enable_fork_support: bool
48
+ attr_accessor tls_config: TLSConfig
49
+ attr_accessor healthcheck: HealthcheckConfig
50
+ attr_accessor otel: OpenTelemetryConfig
51
+
52
+ def initialize: (
53
+ ?token: String,
54
+ ?tenant_id: String,
55
+ ?host_port: String,
56
+ ?server_url: String,
57
+ ?namespace: String,
58
+ ?logger: Logger,
59
+ ?listener_v2_timeout: Integer,
60
+ ?grpc_max_recv_message_length: Integer,
61
+ ?grpc_max_send_message_length: Integer,
62
+ ?worker_preset_labels: Hash[String, String],
63
+ ?enable_force_kill_sync_threads: bool,
64
+ ?enable_thread_pool_monitoring: bool,
65
+ ?terminate_worker_after_num_tasks: Integer,
66
+ ?disable_log_capture: bool,
67
+ ?grpc_enable_fork_support: bool,
68
+ ?tls_config: TLSConfig,
69
+ ?healthcheck: HealthcheckConfig,
70
+ ?otel: OpenTelemetryConfig
71
+ ) -> void
72
+
73
+ def apply_namespace: (String? resource_name, ?namespace_override: String?) -> String?
74
+ def hash: () -> Integer
75
+ def to_h: () -> Hash[Symbol, untyped]
76
+ end
77
+
78
+ class TLSConfig
79
+ attr_accessor strategy: String
80
+ attr_accessor cert_file: String?
81
+ attr_accessor key_file: String?
82
+ attr_accessor root_ca_file: String?
83
+ attr_accessor server_name: String
84
+
85
+ def initialize: (
86
+ ?strategy: String,
87
+ ?cert_file: String,
88
+ ?key_file: String,
89
+ ?root_ca_file: String,
90
+ ?server_name: String
91
+ ) -> void
92
+ end
93
+
94
+ class HealthcheckConfig
95
+ attr_accessor port: Integer
96
+ attr_accessor enabled: bool
97
+
98
+ def initialize: (?port: Integer, ?enabled: bool) -> void
99
+ end
100
+
101
+ class OpenTelemetryConfig
102
+ attr_accessor excluded_attributes: Array[String]
103
+
104
+ def initialize: (?excluded_attributes: Array[String]) -> void
105
+ end
106
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hatchet-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabriel ruttner
@@ -54,6 +54,7 @@ files:
54
54
  - README.md
55
55
  - Rakefile
56
56
  - lib/hatchet-sdk.rb
57
+ - lib/hatchet/config.rb
57
58
  - lib/hatchet/version.rb
58
59
  - sig/hatchet-sdk.rbs
59
60
  homepage: https://hatchet.run