kessel-sdk 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f43583b533d5d6a129442d7ec9ace5ff0a868873825202638b49fe249aa84dc4
4
- data.tar.gz: 3df5445af055fc5f47f092dce4c374a1aa55fa7a62e5d33d85759d4563c2a03f
3
+ metadata.gz: d150b6fc61112dddb8c412f6335ab567d09d44c47397dafcc7a81b0ec36743fa
4
+ data.tar.gz: c79b0372619792f47b8909fe559944af8d19e0a33f67c8ac3525f2b44b3693fb
5
5
  SHA512:
6
- metadata.gz: fc5b79adf8619a5a3e7046f915fb0216da469f98e389a3d4cf7b938959fe6376d9626d7592bdd4d275c8716e2ebdcc5cfc9fae4dc7f73c0b87a0952fb9199682
7
- data.tar.gz: 0bae7e47c009b42b469dbda597785837d0a476b00b10d775c441653da7ec2019f682c20e2ddb6839d7155ee7f200897009b93651b99f9f60bfb89df777cc3c45
6
+ metadata.gz: 269123d3b2699c7dc0d8ad3329dbc7d03751c3dda6ff6a52e908f2a242867d7d4fc69770a1da37fb35972d17a9d7e588607bb97364530005822285c55769eb44
7
+ data.tar.gz: 6623de8660da4b3ed71deb2ae2fa4a96093a543a6fdba3c06796d48e2aa124b2eaf06603564e50ab25c663566374503d5f9230e4d3bb10f290cf0667e17c426e
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Kessel SDK for Ruby
2
2
 
3
+ [![CI](https://github.com/project-kessel/kessel-sdk-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/project-kessel/kessel-sdk-ruby/actions/workflows/ci.yml)
4
+
3
5
  A Ruby gRPC library for connecting to [Project Kessel](https://github.com/project-kessel) services. This provides the foundational gRPC client library for Kessel Inventory API, with plans for a higher-level SDK with fluent APIs, OAuth support, and advanced features in future releases.
4
6
 
5
7
  ## Installation
@@ -22,6 +24,15 @@ Or install it yourself as:
22
24
  gem install kessel-sdk
23
25
  ```
24
26
 
27
+ ## Authentication (Optional)
28
+
29
+ The SDK supports OAuth 2.0 Client Credentials flow. To use authentication features, add the OpenID Connect gem:
30
+
31
+ ```ruby
32
+ gem 'kessel-sdk'
33
+ gem 'openid_connect', '~> 2.0' # Optional - only for authentication
34
+ ```
35
+
25
36
  ## Usage
26
37
 
27
38
  This library provides direct access to Kessel Inventory API gRPC services. All generated classes are available under the `Kessel::Inventory` module.
@@ -130,11 +141,22 @@ All protobuf message classes are generated and available. Key classes include:
130
141
 
131
142
  See the `examples/` directory for complete working examples.
132
143
 
144
+ ## Type Safety
145
+
146
+ This library includes RBS type signatures for enhanced type safety in Ruby. The type definitions are located in the `sig/` directory and cover:
147
+
148
+ - Core library interfaces
149
+ - Configuration structures
150
+ - OAuth authentication classes
151
+ - gRPC client builders
152
+
153
+ To use with type checkers like Steep or Sorbet, ensure the `sig/` directory is in your type checking configuration.
154
+
133
155
  ## Development
134
156
 
135
157
  ### Prerequisites
136
158
 
137
- - Ruby 3.3 or higher
159
+ - Ruby 3.3 or higher
138
160
  - [buf](https://buf.build) for protobuf/gRPC code generation
139
161
 
140
162
  Install buf:
@@ -158,6 +180,22 @@ bundle install
158
180
  buf generate
159
181
  ```
160
182
 
183
+ ### Testing
184
+
185
+ ```bash
186
+ # Run tests
187
+ bundle exec rspec
188
+
189
+ # Run with coverage
190
+ COVERAGE=1 bundle exec rspec
191
+
192
+ # Run linting
193
+ bundle exec rubocop
194
+
195
+ # Security audit
196
+ bundle exec bundle-audit
197
+ ```
198
+
161
199
  ### Code Generation
162
200
 
163
201
  This library uses [buf](https://buf.build) to generate Ruby gRPC code from the official Kessel Inventory API protobuf definitions hosted at `buf.build/project-kessel/inventory-api`.
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc'
4
+ require 'kessel/version'
5
+
6
+ module Kessel
7
+ # OpenID Connect authentication module for Kessel services.
8
+ #
9
+ # This module provides OIDC Client Credentials flow authentication
10
+ # with automatic discovery. Works seamlessly with OIDC-compliant providers.
11
+ #
12
+ # @example Basic usage
13
+ # auth = Kessel::Auth::OAuth2ClientCredentials.new.new(
14
+ # client_id: 'my-app',
15
+ # client_secret: 'secret',
16
+ # token_endpoint: 'https://my-domain/auth/realms/my-realm/protocol/openid-connect/token'
17
+ # )
18
+ # token = auth.get_token
19
+ #
20
+ # @author Project Kessel
21
+ # @since 1.0.0
22
+ module Auth
23
+ EXPIRATION_WINDOW = 300 # 5 minutes in seconds
24
+ DEFAULT_EXPIRES_IN = 3600 # 1 hour in seconds
25
+
26
+ # Exception raised when OAuth functionality is requested but dependencies are missing.
27
+ class OAuthDependencyError < StandardError
28
+ # Creates a new OAuth dependency error.
29
+ #
30
+ # @param message [String] Error message describing the missing dependency
31
+ def initialize(message = 'OAuth functionality requires the openid_connect gem')
32
+ super
33
+ end
34
+ end
35
+
36
+ # Exception raised when OAuth authentication fails.
37
+ class OAuthAuthenticationError < StandardError
38
+ # Creates a new OAuth authentication error.
39
+ #
40
+ # @param message [String] Error message describing the authentication failure
41
+ def initialize(message = 'OAuth authentication failed')
42
+ super
43
+ end
44
+ end
45
+
46
+ OIDCDiscoveryMetadata = Struct.new(:token_endpoint)
47
+ RefreshTokenResponse = Struct.new(:access_token, :expires_at)
48
+
49
+ def fetch_oidc_discovery(provider_url)
50
+ check_dependencies!
51
+ discovery = ::OpenIDConnect::Discovery::Provider::Config.discover!(provider_url)
52
+ OIDCDiscoveryMetadata.new(discovery.token_endpoint)
53
+ rescue StandardError => e
54
+ raise OAuthAuthenticationError, "Failed to discover OIDC configuration from #{provider_url}: #{e.message}"
55
+ end
56
+
57
+ # Checks if the openid_connect gem is available.
58
+ #
59
+ # @raise [OAuthDependencyError] if openid_connect gem is missing
60
+ # @api private
61
+ private
62
+
63
+ def check_dependencies!
64
+ require 'openid_connect'
65
+ rescue LoadError
66
+ raise OAuthDependencyError,
67
+ 'OAuth functionality requires the openid_connect gem. Add "gem \'openid_connect\'" to your Gemfile.'
68
+ end
69
+
70
+ # OpenID Connect Client Credentials flow implementation using discovery.
71
+ #
72
+ # This provides a secure OIDC Client Credentials flow implementation with
73
+ # automatic endpoint discovery. Works seamlessly with OIDC-compliant providers
74
+ # that support discovery.
75
+ #
76
+ # @example
77
+ # oauth = OAuth2ClientCredentials.new(
78
+ # client_id: 'kessel-client',
79
+ # client_secret: 'super-secret-key',
80
+ # token_endpoint: 'https://my-domain/auth/realms/my-realm/protocol/openid-connect/token'
81
+ # )
82
+ #
83
+ # # Get current access token (automatically cached and refreshed)
84
+ # token = oauth.get_token
85
+ class OAuth2ClientCredentials
86
+ include Kessel::Auth
87
+
88
+ # Creates a new OIDC client with specified token endpoint.
89
+ #
90
+ # @param client_id [String] OIDC client identifier
91
+ # @param client_secret [String] OIDC client secret
92
+ # @param token_endpoint [String] OIDC token endpoint URL
93
+ #
94
+ # @raise [OAuthDependencyError] if the openid_connect gem is not available
95
+ # @raise [OAuthAuthenticationError] if authentication fails
96
+ #
97
+ # @example
98
+ # oauth = OAuth2ClientCredentials.new(
99
+ # client_id: 'my-app',
100
+ # client_secret: 'secret',
101
+ # token_endpoint: 'https://my-domain/auth/realms/my-realm/protocol/openid-connect/token'
102
+ # )
103
+ def initialize(client_id:, client_secret:, token_endpoint:)
104
+ check_dependencies!
105
+
106
+ @client_id = client_id
107
+ @client_secret = client_secret
108
+ @token_endpoint = token_endpoint
109
+ @token_mutex = Mutex.new
110
+ end
111
+
112
+ # Gets the current access token with automatic caching and refresh.
113
+ #
114
+ # Uses OIDC Client Credentials flow with automatic token caching,
115
+ # expiration checking, and refresh logic.
116
+ #
117
+ # @return [RefreshTokenResponse] A valid access token
118
+ # @raise [OAuthAuthenticationError] if token acquisition fails
119
+ #
120
+ # @example
121
+ # token = oauth.get_token
122
+ # # Use token in Authorization header: "Bearer #{token}"
123
+ def get_token(force_refresh: false)
124
+ return @cached_token if !force_refresh && token_valid?
125
+
126
+ @token_mutex.synchronize do
127
+ @cached_token = nil if force_refresh
128
+
129
+ # Double-check: another thread might have refreshed the token
130
+ return @cached_token if token_valid?
131
+
132
+ @cached_token = refresh
133
+
134
+ return @cached_token
135
+ rescue StandardError => e
136
+ raise OAuthAuthenticationError, "Failed to obtain client credentials token: #{e.message}"
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def refresh
143
+ client = create_oidc_client
144
+
145
+ request_params = {
146
+ grant_type: 'client_credentials',
147
+ client_id: @client_id,
148
+ client_secret: @client_secret
149
+ }
150
+
151
+ token_data = client.access_token!(request_params)
152
+ RefreshTokenResponse.new(
153
+ access_token: token_data.access_token,
154
+ expires_at: Time.now + (token_data.expires_in || DEFAULT_EXPIRES_IN)
155
+ ).freeze
156
+ end
157
+
158
+ # Checks if we have a valid cached token.
159
+ #
160
+ # @return [Boolean] true if token exists and not expired
161
+ def token_valid?
162
+ return false unless @cached_token
163
+
164
+ expires_at = @cached_token['expires_at']
165
+ return false unless expires_at
166
+
167
+ Time.now.to_i + EXPIRATION_WINDOW < expires_at.to_i
168
+ rescue StandardError
169
+ false
170
+ end
171
+
172
+ # Creates an OIDC client using discovered configuration.
173
+ #
174
+ # @return [OpenIDConnect::Client] Configured OIDC client
175
+ # @api private
176
+ def create_oidc_client
177
+ ::OpenIDConnect::Client.new(
178
+ identifier: @client_id,
179
+ secret: @client_secret,
180
+ token_endpoint: @token_endpoint
181
+ )
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kessel/auth'
4
+ require 'grpc'
5
+
6
+ module Kessel
7
+ module GRPC
8
+ def oauth2_call_credentials(auth)
9
+ call_credentials_proc = proc do |metadata|
10
+ token = auth.get_token
11
+ metadata.merge('authorization' => "Bearer #{token.access_token}")
12
+ end
13
+ ::GRPC::Core::CallCredentials.new(call_credentials_proc)
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kessel
2
4
  module Inventory
3
- VERSION = "1.0.0".freeze
5
+ VERSION = '1.1.0'
4
6
  end
5
7
  end
data/lib/kessel-sdk.rb CHANGED
@@ -1,3 +1,9 @@
1
- Dir.glob(File.join(__dir__, '**', '*_services_pb.rb')).sort.each do |file|
1
+ # frozen_string_literal: true
2
+
3
+ require 'kessel/version'
4
+ require 'kessel/grpc'
5
+ require 'kessel/auth'
6
+
7
+ Dir.glob(File.join(__dir__, '**', '*_services_pb.rb')).each do |file|
2
8
  require file.sub(__dir__ + File::SEPARATOR, '')
3
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kessel-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Project Kessel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-16 00:00:00.000000000 Z
11
+ date: 2025-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grpc
@@ -24,6 +24,174 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.73.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-expectations
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-mocks
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.22'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.22'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.57'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.57'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rack
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rackup
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.1'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.1'
125
+ - !ruby/object:Gem::Dependency
126
+ name: redcarpet
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.6'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.6'
139
+ - !ruby/object:Gem::Dependency
140
+ name: webrick
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.8'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.8'
153
+ - !ruby/object:Gem::Dependency
154
+ name: yard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.9'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.9'
167
+ - !ruby/object:Gem::Dependency
168
+ name: steep
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 1.10.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 1.10.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: typeprof
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 0.30.1
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 0.30.1
27
195
  description: This is the official Ruby SDK for [Project Kessel](https://github.com/project-kessel),
28
196
  a system for unifying APIs and experiences with fine-grained authorization, common
29
197
  inventory, and CloudEvents.
@@ -41,6 +209,8 @@ files:
41
209
  - lib/google/api/field_behavior_pb.rb
42
210
  - lib/google/api/http_pb.rb
43
211
  - lib/kessel-sdk.rb
212
+ - lib/kessel/auth.rb
213
+ - lib/kessel/grpc.rb
44
214
  - lib/kessel/inventory/v1/health_pb.rb
45
215
  - lib/kessel/inventory/v1/health_services_pb.rb
46
216
  - lib/kessel/inventory/v1beta1/authz/check_pb.rb
@@ -94,8 +264,8 @@ licenses:
94
264
  - Apache-2.0
95
265
  metadata:
96
266
  homepage_uri: https://github.com/project-kessel/kessel-sdk-ruby
97
- source_code_uri: https://github.com/project-kessel/kessel-sdk-ruby
98
267
  bug_tracker_uri: https://github.com/project-kessel/kessel-sdk-ruby/issues
268
+ rubygems_mfa_required: 'true'
99
269
  post_install_message:
100
270
  rdoc_options: []
101
271
  require_paths: