traylinx_auth_client 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 +7 -0
- data/PUBLISHING.md +85 -0
- data/README.md +180 -0
- data/lib/traylinx_auth_client/client.rb +377 -0
- data/lib/traylinx_auth_client/configuration.rb +48 -0
- data/lib/traylinx_auth_client/errors.rb +26 -0
- data/lib/traylinx_auth_client/middleware.rb +65 -0
- data/lib/traylinx_auth_client/version.rb +5 -0
- data/lib/traylinx_auth_client.rb +34 -0
- metadata +194 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d479f011cde37bff0d312edef21f32c3cab5e7947971f57c5d165aa1e6b57df3
|
|
4
|
+
data.tar.gz: c5016a5a0bef8c57563210630a488d8196d8ce835b44c2d466710434440b8210
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 263b96fef7fe57aa4b69ea3f2c5399bf78cd492f84ab7aa0ef7e8fd442a3e9b7126a4709b6bb17cdd2b2cf46ddd2429a4a10bd067b53579a5b9c84b8007f3709
|
|
7
|
+
data.tar.gz: 5d1d2659cfb940f4713fa4321e45a22444dfe65f455bf693896739bbd85c042f06d7cb7e8c6bccfab99711bbc67dc2624d454d4f3449431b5b2707eee4366643
|
data/PUBLISHING.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Publishing `traylinx_auth_client`
|
|
2
|
+
|
|
3
|
+
This guide details how to build and publish the `traylinx_auth_client` Ruby gem.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Ruby installed (2.7+)
|
|
8
|
+
- `bundler` installed
|
|
9
|
+
- Access to the target Gem repository (RubyGems.org or private)
|
|
10
|
+
|
|
11
|
+
## 1. Verify Metadata
|
|
12
|
+
|
|
13
|
+
Ensure `traylinx_auth_client.gemspec` has the correct version and metadata.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Check version
|
|
17
|
+
grep "spec.version" traylinx_auth_client.gemspec
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 2. Build the Gem
|
|
21
|
+
|
|
22
|
+
Navigate to the SDK directory and build the gem package.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd traylinx_core/sdks/ruby_sentinel
|
|
26
|
+
gem build traylinx_auth_client.gemspec
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This will output a file named like `traylinx_auth_client-0.1.0.gem`.
|
|
30
|
+
|
|
31
|
+
## 3. Local Verification (Optional)
|
|
32
|
+
|
|
33
|
+
You can install the built gem locally to verify it.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install ./traylinx_auth_client-0.1.0.gem
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 4. Publish to RubyGems
|
|
40
|
+
|
|
41
|
+
If publishing to the public RubyGems.org:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
gem push traylinx_auth_client-0.1.0.gem
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 5. Publish to Private Registry (e.g., GitHub Packages)
|
|
48
|
+
|
|
49
|
+
If using GitHub Packages, you need to configure your `~/.gem/credentials` or use the command line.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Example for GitHub Packages
|
|
53
|
+
gem push --key github --host https://rubygems.pkg.github.com/StartTraylinx traylinx_auth_client-0.1.0.gem
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 6. Automating with CI/CD
|
|
57
|
+
|
|
58
|
+
For automated publishing via GitHub Actions, ensure you have the `RUBYGEMS_API_KEY` secret set up in your repository.
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
# .github/workflows/publish_gem.yml
|
|
62
|
+
name: Publish Ruby Gem
|
|
63
|
+
|
|
64
|
+
on:
|
|
65
|
+
push:
|
|
66
|
+
tags:
|
|
67
|
+
- 'v*'
|
|
68
|
+
|
|
69
|
+
jobs:
|
|
70
|
+
build:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v2
|
|
74
|
+
- name: Set up Ruby
|
|
75
|
+
uses: ruby/setup-ruby@v1
|
|
76
|
+
with:
|
|
77
|
+
ruby-version: '3.0'
|
|
78
|
+
|
|
79
|
+
- name: Build and Publish
|
|
80
|
+
env:
|
|
81
|
+
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
82
|
+
run: |
|
|
83
|
+
gem build *.gemspec
|
|
84
|
+
gem push *.gem
|
|
85
|
+
```
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Traylinx Auth Client (Ruby)
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/traylinx-auth-client)
|
|
4
|
+
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A robust, enterprise-grade Ruby library for Traylinx Sentinel Agent-to-Agent (A2A) authentication. This client provides secure token management, automatic retry logic, comprehensive error handling, and Rack middleware integration.
|
|
8
|
+
|
|
9
|
+
## 🚀 Features
|
|
10
|
+
|
|
11
|
+
- **🔐 Dual Token Authentication**: Handles both `access_token` and `agent_secret_token` with automatic refresh
|
|
12
|
+
- **🛡️ Enterprise Security**: Input validation, secure credential handling, and comprehensive error management
|
|
13
|
+
- **⚡ High Performance**: Thread-safe implementation, token caching, and connection pooling
|
|
14
|
+
- **🔄 Async Support**: Built on Faraday for flexible adapters (Async/EM possible)
|
|
15
|
+
- **🎯 Rack Integration**: Simple middleware for protecting endpoints with A2A authentication
|
|
16
|
+
- **📡 JSON-RPC Support**: Support for A2A RPC method calls
|
|
17
|
+
- **🔧 Zero Configuration**: Works with environment variables out of the box
|
|
18
|
+
|
|
19
|
+
## 📦 Installation
|
|
20
|
+
|
|
21
|
+
Add this line to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'traylinx_auth_client'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
And then execute:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install it yourself as:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install traylinx_auth_client
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ⚡ Quick Start (5 lines)
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require 'traylinx_auth_client'
|
|
43
|
+
|
|
44
|
+
# Set ENV: TRAYLINX_CLIENT_ID, TRAYLINX_CLIENT_SECRET,
|
|
45
|
+
# TRAYLINX_API_BASE_URL, TRAYLINX_AGENT_USER_ID
|
|
46
|
+
|
|
47
|
+
# Make authenticated request to another agent
|
|
48
|
+
client = TraylinxAuthClient.client
|
|
49
|
+
response = client.make_a2a_request(:get, "https://other-agent.com/api/data")
|
|
50
|
+
puts response # JSON response body
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 🔧 Configuration
|
|
54
|
+
|
|
55
|
+
### Environment Variables
|
|
56
|
+
|
|
57
|
+
Set these environment variables for your agent:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export TRAYLINX_CLIENT_ID="your-client-id"
|
|
61
|
+
export TRAYLINX_CLIENT_SECRET="your-client-secret"
|
|
62
|
+
export TRAYLINX_API_BASE_URL="https://auth.traylinx.com"
|
|
63
|
+
export TRAYLINX_AGENT_USER_ID="12345678-..."
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Programmatic Configuration
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
require 'traylinx_auth_client'
|
|
70
|
+
|
|
71
|
+
TraylinxAuthClient.configure do |config|
|
|
72
|
+
config.client_id = "your-client-id"
|
|
73
|
+
config.client_secret = "your-client-secret"
|
|
74
|
+
config.api_base_url = "https://auth.traylinx.com"
|
|
75
|
+
config.agent_user_id = "1234..."
|
|
76
|
+
|
|
77
|
+
config.timeout = 30 # Seconds
|
|
78
|
+
config.max_retries = 3
|
|
79
|
+
config.cache_tokens = true
|
|
80
|
+
config.log_level = :info
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 📖 Usage Examples
|
|
85
|
+
|
|
86
|
+
### Making Authenticated Requests
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
client = TraylinxAuthClient.client
|
|
90
|
+
|
|
91
|
+
# GET request
|
|
92
|
+
data = client.make_a2a_request(:get, "https://other-agent.com/api/users")
|
|
93
|
+
|
|
94
|
+
# POST request with JSON
|
|
95
|
+
result = client.make_a2a_request(:post, "https://other-agent.com/api/process",
|
|
96
|
+
json: { items: ['item1', 'item2'] },
|
|
97
|
+
timeout: 60
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Manual Header Management
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Get all headers (for Auth Service)
|
|
105
|
+
headers = client.get_request_headers
|
|
106
|
+
# => { "Authorization" => "Bearer ...", "X-Agent-Secret-Token" => "...", ... }
|
|
107
|
+
|
|
108
|
+
# Get Agent headers (for A2A)
|
|
109
|
+
agent_headers = client.get_agent_request_headers
|
|
110
|
+
# => { "X-Agent-Secret-Token" => "...", "X-Agent-User-Id" => "..." }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Rack Middleware (Rails/Sinatra)
|
|
114
|
+
|
|
115
|
+
Protect your application endpoints with A2A authentication.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# config.ru or application.rb
|
|
119
|
+
require 'traylinx_auth_client'
|
|
120
|
+
|
|
121
|
+
use TraylinxAuthClient::Middleware
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or with specific validation path:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
use TraylinxAuthClient::Middleware, validation_path: /^\/api\/protected/
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### JSON-RPC Calls
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Introspect a token
|
|
134
|
+
active = client.validate_token("some-agent-token", "agent-user-id")
|
|
135
|
+
|
|
136
|
+
# Generic RPC call
|
|
137
|
+
result = client.rpc_call("custom_method", { param: "value" })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 🌌 Stargate P2P Identity (New)
|
|
141
|
+
|
|
142
|
+
The client supports Stargate P2P identity certification for peer-to-peer communication.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# 1. Get a challenge from Sentinel
|
|
146
|
+
challenge = client.get_p2p_challenge(peer_id)
|
|
147
|
+
|
|
148
|
+
# 2. Sign the challenge with your private key (implementation depends on your crypto lib)
|
|
149
|
+
signature = sign_challenge(challenge, private_key)
|
|
150
|
+
|
|
151
|
+
# 3. Certify identity
|
|
152
|
+
cert = client.certify_p2p_identity(peer_id, public_key_base64, signature, challenge)
|
|
153
|
+
|
|
154
|
+
puts cert[:certificate] # JWT Certificate
|
|
155
|
+
puts cert[:expires_at] # Expiration
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 🔐 Authentication Flow
|
|
159
|
+
|
|
160
|
+
1. **Token Acquisition**: Automatically exchanges `client_id` + `client_secret` for OAuth tokens.
|
|
161
|
+
2. **Token Caching**: Caches `access_token` and `agent_secret_token` in a thread-safe Map.
|
|
162
|
+
3. **Auto Refresh**: Refreshes tokens 60 seconds before expiration.
|
|
163
|
+
|
|
164
|
+
## 🛡️ Error Handling
|
|
165
|
+
|
|
166
|
+
The client raises specific exceptions rooted in `TraylinxAuthClient::TraylinxAuthError`.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
begin
|
|
170
|
+
client.make_a2a_request(:get, url)
|
|
171
|
+
rescue TraylinxAuthClient::AuthenticationError => e
|
|
172
|
+
puts "Auth failed: #{e.message}"
|
|
173
|
+
rescue TraylinxAuthClient::NetworkError => e
|
|
174
|
+
puts "Network error: #{e.message}"
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT License.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "concurrent"
|
|
6
|
+
require "json"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
require_relative "configuration"
|
|
10
|
+
require_relative "errors"
|
|
11
|
+
|
|
12
|
+
module TraylinxAuthClient
|
|
13
|
+
# Main client for interacting with Traylinx Sentinel Authentication.
|
|
14
|
+
# Handles token acquisition, management, and validation.
|
|
15
|
+
class Client
|
|
16
|
+
attr_reader :config
|
|
17
|
+
|
|
18
|
+
# Initializes the Client with configuration options.
|
|
19
|
+
#
|
|
20
|
+
# @param options [Hash, Configuration] Hash of options or Configuration object
|
|
21
|
+
# @option options [String] :client_id
|
|
22
|
+
# @option options [String] :client_secret
|
|
23
|
+
# @option options [String] :api_base_url
|
|
24
|
+
# @option options [String] :agent_user_id
|
|
25
|
+
def initialize(options = {})
|
|
26
|
+
@config = options.is_a?(Configuration) ? options : Configuration.new(options)
|
|
27
|
+
|
|
28
|
+
# Token storage
|
|
29
|
+
@tokens = Concurrent::Map.new
|
|
30
|
+
@tokens[:access_token] = nil
|
|
31
|
+
@tokens[:agent_secret_token] = nil
|
|
32
|
+
@tokens[:expires_at] = nil
|
|
33
|
+
|
|
34
|
+
# Mutex for token refresh synchronization
|
|
35
|
+
@refresh_mutex = Mutex.new
|
|
36
|
+
|
|
37
|
+
setup_connection
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieves a valid Access Token (for Auth Service interaction).
|
|
41
|
+
# Refreshes the token if expired.
|
|
42
|
+
#
|
|
43
|
+
# @return [String] The JWT access token
|
|
44
|
+
# @raise [TokenExpiredError] if token cannot be obtained
|
|
45
|
+
def get_access_token
|
|
46
|
+
ensure_valid_tokens!
|
|
47
|
+
token = @tokens[:access_token]
|
|
48
|
+
raise TokenExpiredError, "Failed to obtain access token" unless token
|
|
49
|
+
token
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Retrieves a valid Agent Secret Token (for A2A communication).
|
|
53
|
+
# Refreshes the token if expired.
|
|
54
|
+
#
|
|
55
|
+
# @return [String] The agent secret token
|
|
56
|
+
# @raise [TokenExpiredError] if token cannot be obtained
|
|
57
|
+
def get_agent_secret_token
|
|
58
|
+
ensure_valid_tokens!
|
|
59
|
+
token = @tokens[:agent_secret_token]
|
|
60
|
+
raise TokenExpiredError, "Failed to obtain agent secret token" unless token
|
|
61
|
+
token
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_request_headers
|
|
65
|
+
{
|
|
66
|
+
"Authorization" => "Bearer #{get_access_token}",
|
|
67
|
+
"X-Agent-Secret-Token" => get_agent_secret_token,
|
|
68
|
+
"X-Agent-User-Id" => @config.agent_user_id
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def get_agent_request_headers
|
|
73
|
+
{
|
|
74
|
+
"X-Agent-Secret-Token" => get_agent_secret_token,
|
|
75
|
+
"X-Agent-User-Id" => @config.agent_user_id
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_a2a_headers
|
|
80
|
+
{
|
|
81
|
+
"Authorization" => "Bearer #{get_agent_secret_token}",
|
|
82
|
+
"X-Agent-User-Id" => @config.agent_user_id
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Helper to perform a validated HTTP request to another agent.
|
|
87
|
+
# Automatically injects A2A headers (X-Agent-Secret-Token).
|
|
88
|
+
#
|
|
89
|
+
# @param method [Symbol] :get, :post, :put, :delete
|
|
90
|
+
# @param url [String] The full URL to call
|
|
91
|
+
# @param options [Hash] Request options
|
|
92
|
+
# @option options [Hash] :headers Additional headers
|
|
93
|
+
# @option options [Hash] :json JSON payload (for POST/PUT)
|
|
94
|
+
# @option options [Hash] :params Query parameters
|
|
95
|
+
# @option options [Integer] :timeout Request timeout
|
|
96
|
+
# @return [Hash, String] The response body (parsed JSON if applicable)
|
|
97
|
+
def make_a2a_request(method, url, options = {})
|
|
98
|
+
headers = get_agent_request_headers.merge(options[:headers] || {})
|
|
99
|
+
|
|
100
|
+
response = Faraday.new(url: url) do |f|
|
|
101
|
+
f.request :json
|
|
102
|
+
f.response :json
|
|
103
|
+
f.adapter Faraday.default_adapter
|
|
104
|
+
end.send(method.to_s.downcase) do |req|
|
|
105
|
+
req.headers = headers
|
|
106
|
+
req.body = options[:json] if options[:json]
|
|
107
|
+
req.params = options[:params] if options[:params]
|
|
108
|
+
req.options.timeout = options[:timeout] || @config.timeout
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
handle_response(response)
|
|
112
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
113
|
+
raise NetworkError.new("Connection failed: #{e.message}", error_code: "CONNECTION_ERROR")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Validates an Agent Secret Token against the Sentinel Introspection endpoint.
|
|
117
|
+
# Usage: When receiving a request from another agent.
|
|
118
|
+
#
|
|
119
|
+
# @param agent_secret_token [String] The token to validate
|
|
120
|
+
# @param agent_user_id [String] The agent ID claiming the token
|
|
121
|
+
# @return [Boolean] true if valid, false otherwise
|
|
122
|
+
def validate_token(agent_secret_token, agent_user_id)
|
|
123
|
+
# Validates against /oauth/agent/introspect using Access Token
|
|
124
|
+
|
|
125
|
+
headers = {
|
|
126
|
+
"Authorization" => "Bearer #{get_access_token}",
|
|
127
|
+
"Content-Type" => "application/x-www-form-urlencoded"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
data = {
|
|
131
|
+
agent_secret_token: agent_secret_token,
|
|
132
|
+
agent_user_id: agent_user_id
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
response = @connection.post("oauth/agent/introspect") do |req|
|
|
136
|
+
req.headers = headers
|
|
137
|
+
req.body = data # Let middleware handle encoding
|
|
138
|
+
req.options.timeout = @config.timeout
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if response.status == 200
|
|
142
|
+
body = response.body.is_a?(String) ? (JSON.parse(response.body) rescue {}) : response.body
|
|
143
|
+
return body["active"] == true
|
|
144
|
+
elsif response.status == 401
|
|
145
|
+
raise AuthenticationError.new("Access token invalid for token validation", status_code: 401)
|
|
146
|
+
else
|
|
147
|
+
handle_response(response) # Will raise appropriate error
|
|
148
|
+
end
|
|
149
|
+
rescue Faraday::Error => e
|
|
150
|
+
raise NetworkError.new("Token validation failed: #{e.message}")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def detect_auth_mode(headers)
|
|
154
|
+
# Normalize headers keys to downcase
|
|
155
|
+
headers = headers.transform_keys { |k| k.to_s.downcase }
|
|
156
|
+
|
|
157
|
+
if headers["authorization"]&.start_with?("Bearer ")
|
|
158
|
+
"bearer"
|
|
159
|
+
elsif headers["x-agent-secret-token"]
|
|
160
|
+
"custom"
|
|
161
|
+
else
|
|
162
|
+
"none"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def validate_a2a_request(headers)
|
|
167
|
+
headers = headers.transform_keys { |k| k.to_s.downcase }
|
|
168
|
+
|
|
169
|
+
# 1. Try Bearer token
|
|
170
|
+
if headers["authorization"]&.start_with?("Bearer ")
|
|
171
|
+
token = headers["authorization"].split(" ").last
|
|
172
|
+
agent_id = headers["x-agent-user-id"]
|
|
173
|
+
return validate_token(token, agent_id) if token && agent_id
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# 2. Try Custom Header
|
|
177
|
+
token = headers["x-agent-secret-token"]
|
|
178
|
+
agent_id = headers["x-agent-user-id"]
|
|
179
|
+
|
|
180
|
+
if token && agent_id
|
|
181
|
+
return validate_token(token, agent_id)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# =========================================================================
|
|
188
|
+
# Stargate P2P Identity Methods
|
|
189
|
+
# =========================================================================
|
|
190
|
+
|
|
191
|
+
def get_p2p_challenge(peer_id)
|
|
192
|
+
headers = {
|
|
193
|
+
"Authorization" => "Bearer #{get_access_token}",
|
|
194
|
+
"Content-Type" => "application/json"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
response = @connection.get("a2a/p2p/challenge") do |req|
|
|
198
|
+
req.params = { peer_id: peer_id }
|
|
199
|
+
req.headers = headers
|
|
200
|
+
req.options.timeout = @config.timeout
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
body = handle_response(response)
|
|
204
|
+
body["challenge"]
|
|
205
|
+
rescue Faraday::Error => e
|
|
206
|
+
raise NetworkError.new("Failed to fetch P2P challenge: #{e.message}")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def certify_p2p_identity(peer_id, public_key, signature, challenge)
|
|
210
|
+
headers = {
|
|
211
|
+
"Authorization" => "Bearer #{get_access_token}",
|
|
212
|
+
"Content-Type" => "application/json"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
payload = {
|
|
216
|
+
peer_id: peer_id,
|
|
217
|
+
public_key: public_key,
|
|
218
|
+
signature: signature,
|
|
219
|
+
challenge: challenge
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
response = @connection.post("a2a/p2p/certify") do |req|
|
|
223
|
+
req.headers = headers
|
|
224
|
+
req.body = payload.to_json
|
|
225
|
+
req.options.timeout = @config.timeout
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
body = handle_response(response)
|
|
229
|
+
{
|
|
230
|
+
certificate: body["certificate"],
|
|
231
|
+
expires_at: body["expires_at"]
|
|
232
|
+
}
|
|
233
|
+
rescue Faraday::Error => e
|
|
234
|
+
raise NetworkError.new("Failed to certify P2P identity: #{e.message}")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Generic RPC call
|
|
238
|
+
def rpc_call(method, params, rpc_url: nil, include_agent_credentials: nil)
|
|
239
|
+
default_rpc_url = "#{@config.api_base_url.chomp('/')}/a2a/rpc"
|
|
240
|
+
url = rpc_url || default_rpc_url
|
|
241
|
+
|
|
242
|
+
# Auto-detect defaults matching Python SDK logic
|
|
243
|
+
if include_agent_credentials.nil?
|
|
244
|
+
# If URL is NOT the default Auth Service RPC URL, assume it's another agent => use agent creds
|
|
245
|
+
include_agent_credentials = (url != default_rpc_url)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
headers = {
|
|
249
|
+
"Content-Type" => "application/json"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if include_agent_credentials
|
|
253
|
+
headers["X-Agent-Secret-Token"] = get_agent_secret_token
|
|
254
|
+
headers["X-Agent-User-Id"] = @config.agent_user_id
|
|
255
|
+
else
|
|
256
|
+
headers["Authorization"] = "Bearer #{get_access_token}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
payload = {
|
|
260
|
+
jsonrpc: "2.0",
|
|
261
|
+
method: method,
|
|
262
|
+
params: params,
|
|
263
|
+
id: SecureRandom.uuid
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
response = Faraday.new(url: url) do |f|
|
|
267
|
+
f.request :json
|
|
268
|
+
f.response :json
|
|
269
|
+
f.adapter Faraday.default_adapter
|
|
270
|
+
end.post do |req|
|
|
271
|
+
req.headers = headers
|
|
272
|
+
req.body = payload
|
|
273
|
+
req.options.timeout = @config.timeout
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
body = handle_response(response)
|
|
277
|
+
|
|
278
|
+
if body.is_a?(Hash) && body["error"]
|
|
279
|
+
raise TraylinxAuthError.new("RPC Error: #{body['error']['message']}", error_code: body['error']['code'])
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
body.is_a?(Hash) ? body["result"] : body
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def setup_connection
|
|
288
|
+
base_url = @config.api_base_url
|
|
289
|
+
base_url = "#{base_url}/" unless base_url.end_with?("/")
|
|
290
|
+
|
|
291
|
+
@connection = Faraday.new(url: base_url) do |f|
|
|
292
|
+
f.request :json
|
|
293
|
+
f.request :url_encoded
|
|
294
|
+
f.response :json
|
|
295
|
+
f.request :retry, {
|
|
296
|
+
max: @config.max_retries,
|
|
297
|
+
interval: @config.retry_delay,
|
|
298
|
+
backoff_factor: 2,
|
|
299
|
+
retry_statuses: [429, 500, 502, 503, 504],
|
|
300
|
+
exceptions: [
|
|
301
|
+
Faraday::ConnectionFailed,
|
|
302
|
+
Faraday::TimeoutError,
|
|
303
|
+
Faraday::SSLError
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
f.adapter Faraday.default_adapter
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def ensure_valid_tokens!
|
|
311
|
+
# return if tokens are valid (and we cache tokens)
|
|
312
|
+
return if @config.cache_tokens && valid_tokens?
|
|
313
|
+
|
|
314
|
+
@refresh_mutex.synchronize do
|
|
315
|
+
# double check inside lock
|
|
316
|
+
return if @config.cache_tokens && valid_tokens?
|
|
317
|
+
refresh_tokens!
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def valid_tokens?
|
|
322
|
+
return false unless @tokens[:access_token]
|
|
323
|
+
return false unless @tokens[:expires_at]
|
|
324
|
+
|
|
325
|
+
# Buffer of 60 seconds
|
|
326
|
+
Time.now.utc < (@tokens[:expires_at] - 60)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def refresh_tokens!
|
|
330
|
+
# Use relative path to ensure correct appending to base URL
|
|
331
|
+
response = @connection.post("oauth/token") do |req|
|
|
332
|
+
req.headers["Content-Type"] = "application/json"
|
|
333
|
+
req.body = {
|
|
334
|
+
grant_type: "client_credentials",
|
|
335
|
+
client_id: @config.client_id,
|
|
336
|
+
client_secret: @config.client_secret,
|
|
337
|
+
scope: "a2a"
|
|
338
|
+
}
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
if response.status == 200
|
|
342
|
+
data = response.body
|
|
343
|
+
# data keys might be strings or symbols depending on middleware
|
|
344
|
+
# Faraday middleware response :json parses with string keys by default usually?
|
|
345
|
+
# Actually standard practice is string keys unless configged
|
|
346
|
+
|
|
347
|
+
# Safe access
|
|
348
|
+
data = data.transform_keys(&:to_s)
|
|
349
|
+
|
|
350
|
+
@tokens[:access_token] = data["access_token"]
|
|
351
|
+
@tokens[:agent_secret_token] = data["agent_secret_token"]
|
|
352
|
+
|
|
353
|
+
expires_in = data["expires_in"].to_i
|
|
354
|
+
@tokens[:expires_at] = Time.now.utc + expires_in
|
|
355
|
+
else
|
|
356
|
+
raise AuthenticationError.new(
|
|
357
|
+
"Valid credentials failed: #{response.body}",
|
|
358
|
+
status_code: response.status
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
rescue Faraday::Error => e
|
|
362
|
+
raise NetworkError.new("Network error during token refresh: #{e.message}")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def handle_response(response)
|
|
366
|
+
if response.success?
|
|
367
|
+
response.body
|
|
368
|
+
elsif response.status == 401
|
|
369
|
+
raise AuthenticationError.new("Authentication failed", status_code: 401)
|
|
370
|
+
elsif response.status == 403
|
|
371
|
+
raise AuthenticationError.new("Permission denied", status_code: 403)
|
|
372
|
+
else
|
|
373
|
+
raise NetworkError.new("Request failed: #{response.status}", status_code: response.status)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module TraylinxAuthClient
|
|
6
|
+
# Configuration class for the client
|
|
7
|
+
class Configuration
|
|
8
|
+
attr_accessor :client_id, :client_secret, :api_base_url, :agent_user_id,
|
|
9
|
+
:timeout, :max_retries, :retry_delay, :cache_tokens, :log_level
|
|
10
|
+
|
|
11
|
+
# Defaults matching Python/JS SDKs
|
|
12
|
+
DEFAULT_TIMEOUT = 30
|
|
13
|
+
DEFAULT_MAX_RETRIES = 3
|
|
14
|
+
DEFAULT_RETRY_DELAY = 1.0
|
|
15
|
+
DEFAULT_CACHE_TOKENS = true
|
|
16
|
+
DEFAULT_LOG_LEVEL = :info
|
|
17
|
+
|
|
18
|
+
def initialize(options = {})
|
|
19
|
+
@client_id = options[:client_id] || ENV["TRAYLINX_CLIENT_ID"]
|
|
20
|
+
@client_secret = options[:client_secret] || ENV["TRAYLINX_CLIENT_SECRET"]
|
|
21
|
+
@api_base_url = options[:api_base_url] || ENV["TRAYLINX_API_BASE_URL"]
|
|
22
|
+
@agent_user_id = options[:agent_user_id] || ENV["TRAYLINX_AGENT_USER_ID"]
|
|
23
|
+
|
|
24
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
|
25
|
+
@max_retries = options[:max_retries] || DEFAULT_MAX_RETRIES
|
|
26
|
+
@retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
|
|
27
|
+
@cache_tokens = options.key?(:cache_tokens) ? options[:cache_tokens] : DEFAULT_CACHE_TOKENS
|
|
28
|
+
@log_level = options[:log_level] || DEFAULT_LOG_LEVEL
|
|
29
|
+
|
|
30
|
+
validate!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate!
|
|
34
|
+
missing = []
|
|
35
|
+
missing << "client_id" unless @client_id
|
|
36
|
+
missing << "client_secret" unless @client_secret
|
|
37
|
+
missing << "api_base_url" unless @api_base_url
|
|
38
|
+
|
|
39
|
+
# agent_user_id is needed for A2A but maybe valid to init without it for Auth
|
|
40
|
+
# service checks? Following strict parity mostly requires it.
|
|
41
|
+
# For now, we'll mark it optional at init but required for A2A calls.
|
|
42
|
+
|
|
43
|
+
unless missing.empty?
|
|
44
|
+
raise ValidationError.new("Missing required configuration: #{missing.join(', ')}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TraylinxAuthClient
|
|
4
|
+
# Base error class for all TraylinxAuthClient errors
|
|
5
|
+
class TraylinxAuthError < StandardError
|
|
6
|
+
attr_reader :error_code, :status_code
|
|
7
|
+
|
|
8
|
+
def initialize(message, error_code: nil, status_code: nil)
|
|
9
|
+
super(message)
|
|
10
|
+
@error_code = error_code
|
|
11
|
+
@status_code = status_code
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Thrown for input validation failures
|
|
16
|
+
class ValidationError < TraylinxAuthError; end
|
|
17
|
+
|
|
18
|
+
# Thrown for authentication-related failures
|
|
19
|
+
class AuthenticationError < TraylinxAuthError; end
|
|
20
|
+
|
|
21
|
+
# Thrown when tokens are expired or unavailable
|
|
22
|
+
class TokenExpiredError < TraylinxAuthError; end
|
|
23
|
+
|
|
24
|
+
# Thrown for network-related issues
|
|
25
|
+
class NetworkError < TraylinxAuthError; end
|
|
26
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client"
|
|
4
|
+
|
|
5
|
+
module TraylinxAuthClient
|
|
6
|
+
# Rack Middleware for protecting endpoints with A2A auth.
|
|
7
|
+
# Intercepts requests and validates Authentication headers.
|
|
8
|
+
class Middleware
|
|
9
|
+
# @param app [Object] The Rack application
|
|
10
|
+
# @param client [TraylinxAuthClient::Client, nil] Optional pre-configured client
|
|
11
|
+
# @param validation_path [Regexp, String, nil] Optional regex/string to match paths requiring auth
|
|
12
|
+
def initialize(app, client: nil, validation_path: nil)
|
|
13
|
+
@app = app
|
|
14
|
+
@client = client || Client.new
|
|
15
|
+
@validation_path = validation_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Handles the incoming Rack request.
|
|
19
|
+
# @param env [Hash] The Rack environment
|
|
20
|
+
def call(env)
|
|
21
|
+
# Only validate if no specific path filtering is set, or if path matches
|
|
22
|
+
# This is simplified. Real middleware often takes an optional block or regex.
|
|
23
|
+
# For now, we assume if mounted, it protects everything unless configured.
|
|
24
|
+
|
|
25
|
+
request = Rack::Request.new(env)
|
|
26
|
+
|
|
27
|
+
# If validation_path is provided, only apply to that path
|
|
28
|
+
if @validation_path && !request.path.match?(@validation_path)
|
|
29
|
+
return @app.call(env)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 1. Construct headers map from Rack environment
|
|
33
|
+
headers = extract_headers(request)
|
|
34
|
+
|
|
35
|
+
# 2. Validate using the unified client logic
|
|
36
|
+
# This handles Bearer tokens and custom headers with correct priority
|
|
37
|
+
if @client.validate_a2a_request(headers)
|
|
38
|
+
@app.call(env)
|
|
39
|
+
else
|
|
40
|
+
unauthorized_response("Invalid authentication credentials")
|
|
41
|
+
end
|
|
42
|
+
rescue => e
|
|
43
|
+
# Log error if possible
|
|
44
|
+
unauthorized_response("Authentication error: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def extract_headers(request)
|
|
50
|
+
# Rack stores headers as HTTP_UPPERCASE_NAME
|
|
51
|
+
# We just need to reconstruct the relevant ones or pass a predictable hash
|
|
52
|
+
# validate_a2a_request handles case-insensitivity, so we just pass keys.
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
"Authorization" => request.get_header("HTTP_AUTHORIZATION"),
|
|
56
|
+
"X-Agent-Secret-Token" => request.get_header("HTTP_X_AGENT_SECRET_TOKEN"),
|
|
57
|
+
"X-Agent-User-Id" => request.get_header("HTTP_X_AGENT_USER_ID")
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unauthorized_response(message)
|
|
62
|
+
[401, { "Content-Type" => "application/json" }, [{ error: message }.to_json]]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "traylinx_auth_client/version"
|
|
4
|
+
require_relative "traylinx_auth_client/configuration"
|
|
5
|
+
require_relative "traylinx_auth_client/client"
|
|
6
|
+
require_relative "traylinx_auth_client/errors"
|
|
7
|
+
require_relative "traylinx_auth_client/middleware"
|
|
8
|
+
|
|
9
|
+
module TraylinxAuthClient
|
|
10
|
+
class << self
|
|
11
|
+
def configure
|
|
12
|
+
yield configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def client
|
|
20
|
+
@client ||= Client.new(configuration)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Reset client and configuration (useful for testing)
|
|
24
|
+
def reset!
|
|
25
|
+
@configuration = nil
|
|
26
|
+
@client = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Proxy helper methods to the default client
|
|
30
|
+
def make_a2a_request(method, url, options = {})
|
|
31
|
+
client.make_a2a_request(method, url, options)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: traylinx_auth_client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Traylinx Team
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.7'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.7'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday-retry
|
|
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'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: concurrent-ruby
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.2'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: jwt
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.7'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.7'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rack
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '2.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: bundler
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '2.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '2.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '13.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '13.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rspec
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: webmock
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '3.18'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '3.18'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: rack-test
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '2.1'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '2.1'
|
|
153
|
+
description: A robust Ruby client for Traylinx Sentinel A2A authentication. Provides
|
|
154
|
+
secure token management, OAuth2 exchange, and Rack middleware.
|
|
155
|
+
email:
|
|
156
|
+
- dev@traylinx.com
|
|
157
|
+
executables: []
|
|
158
|
+
extensions: []
|
|
159
|
+
extra_rdoc_files: []
|
|
160
|
+
files:
|
|
161
|
+
- PUBLISHING.md
|
|
162
|
+
- README.md
|
|
163
|
+
- lib/traylinx_auth_client.rb
|
|
164
|
+
- lib/traylinx_auth_client/client.rb
|
|
165
|
+
- lib/traylinx_auth_client/configuration.rb
|
|
166
|
+
- lib/traylinx_auth_client/errors.rb
|
|
167
|
+
- lib/traylinx_auth_client/middleware.rb
|
|
168
|
+
- lib/traylinx_auth_client/version.rb
|
|
169
|
+
homepage: https://github.com/traylinx/traylinx-auth-client-ruby
|
|
170
|
+
licenses:
|
|
171
|
+
- MIT
|
|
172
|
+
metadata:
|
|
173
|
+
homepage_uri: https://github.com/traylinx/traylinx-auth-client-ruby
|
|
174
|
+
source_code_uri: https://github.com/traylinx/traylinx-auth-client-ruby
|
|
175
|
+
post_install_message:
|
|
176
|
+
rdoc_options: []
|
|
177
|
+
require_paths:
|
|
178
|
+
- lib
|
|
179
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
180
|
+
requirements:
|
|
181
|
+
- - ">="
|
|
182
|
+
- !ruby/object:Gem::Version
|
|
183
|
+
version: 2.7.0
|
|
184
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
185
|
+
requirements:
|
|
186
|
+
- - ">="
|
|
187
|
+
- !ruby/object:Gem::Version
|
|
188
|
+
version: '0'
|
|
189
|
+
requirements: []
|
|
190
|
+
rubygems_version: 3.4.10
|
|
191
|
+
signing_key:
|
|
192
|
+
specification_version: 4
|
|
193
|
+
summary: Traylinx Sentinel Agent-to-Agent Authentication Client
|
|
194
|
+
test_files: []
|