keyenv 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 +7 -0
- data/CHANGELOG.md +39 -0
- data/LICENSE +21 -0
- data/README.md +245 -0
- data/lib/keyenv/client.rb +492 -0
- data/lib/keyenv/error.rb +56 -0
- data/lib/keyenv/types.rb +176 -0
- data/lib/keyenv/version.rb +5 -0
- data/lib/keyenv.rb +44 -0
- metadata +155 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b674e0b30a5f7d531fab1e1029ee40fc1706633030c44227e8fbcbd684334b39
|
|
4
|
+
data.tar.gz: 62d26d5f098f32e81341929bb86204bafb7543d7df22efc883f666b01b3ab182
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b7d130375c1a7b6d275c8dfa9d8cdc82a88e0f593db5ec6253e6d2a0e7040ddfb7a24d516881b795a6ce6accd58d046a43219d1b3680a30ed89d127b8e1015c3
|
|
7
|
+
data.tar.gz: 6cb4ab4ba2e5b1f2b962c899cbb868b738dab39c80c26ff2796a1527dd1fa59972536179ec1406d14cf50e6f38867645ef4c169fc5dba560413adb2184ec4d62
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.2.0](https://github.com/keyenv/ruby-sdk/compare/v1.1.0...v1.2.0) (2026-01-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add integration tests for live API testing ([c4f47c2](https://github.com/keyenv/ruby-sdk/commit/c4f47c2aa94492975a9f50ebd78287f00c2069d7))
|
|
14
|
+
* add tag-based release workflow for RubyGems ([736a6aa](https://github.com/keyenv/ruby-sdk/commit/736a6aa9ce190b0494e1c39c78820feba0e813a6))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* correct error message formatting ([dbe213e](https://github.com/keyenv/ruby-sdk/commit/dbe213e2b59788a6189720e6dbb17bde03b13939))
|
|
20
|
+
|
|
21
|
+
## [1.0.0] - 2025-01-23
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Initial release
|
|
26
|
+
- `KeyEnv.new(token:)` and `KeyEnv.create(token)` client creation
|
|
27
|
+
- Authentication: `get_current_user`, `validate_token`
|
|
28
|
+
- Projects: `list_projects`, `get_project`, `create_project`, `delete_project`
|
|
29
|
+
- Environments: `list_environments`, `create_environment`, `delete_environment`
|
|
30
|
+
- Secrets: `list_secrets`, `export_secrets`, `export_secrets_as_hash`, `get_secret`
|
|
31
|
+
- Secret management: `create_secret`, `update_secret`, `set_secret`, `delete_secret`
|
|
32
|
+
- Bulk operations: `bulk_import`, `get_secret_history`
|
|
33
|
+
- Utilities: `load_env`, `generate_env_file`, `clear_cache`
|
|
34
|
+
- Permissions: `list_permissions`, `set_permission`, `delete_permission`, `bulk_set_permissions`
|
|
35
|
+
- Permission queries: `get_my_permissions`, `get_project_defaults`, `set_project_defaults`
|
|
36
|
+
- Built-in caching with configurable TTL for serverless environments
|
|
37
|
+
- Typed data classes for all API responses
|
|
38
|
+
- Specific error classes: `AuthenticationError`, `NotFoundError`, `ValidationError`, `RateLimitError`
|
|
39
|
+
- Full RSpec test suite with WebMock
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 KeyEnv
|
|
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,245 @@
|
|
|
1
|
+
# KeyEnv Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for [KeyEnv](https://keyenv.dev) - Secure secrets management for development teams.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'keyenv'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install keyenv
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'keyenv'
|
|
29
|
+
|
|
30
|
+
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
|
|
31
|
+
|
|
32
|
+
# Load secrets into ENV
|
|
33
|
+
client.load_env(project_id: 'your-project-id', environment: 'production')
|
|
34
|
+
puts ENV['DATABASE_URL']
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Initialize the Client
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require 'keyenv'
|
|
43
|
+
|
|
44
|
+
# Using keyword argument
|
|
45
|
+
client = KeyEnv.new(token: 'your-service-token')
|
|
46
|
+
|
|
47
|
+
# Alternative syntax
|
|
48
|
+
client = KeyEnv.create('your-service-token')
|
|
49
|
+
|
|
50
|
+
# With custom timeout and caching
|
|
51
|
+
client = KeyEnv.new(token: 'your-token', timeout: 60, cache_ttl: 300)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Export Secrets
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# Get all secrets as a list
|
|
58
|
+
secrets = client.export_secrets(project_id: 'proj_123', environment: 'production')
|
|
59
|
+
secrets.each do |secret|
|
|
60
|
+
puts "#{secret.key}=#{secret.value}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get secrets as a hash
|
|
64
|
+
env = client.export_secrets_as_hash(project_id: 'proj_123', environment: 'production')
|
|
65
|
+
puts env['DATABASE_URL']
|
|
66
|
+
|
|
67
|
+
# Load directly into ENV
|
|
68
|
+
count = client.load_env(project_id: 'proj_123', environment: 'production')
|
|
69
|
+
puts "Loaded #{count} secrets"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Manage Secrets
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Get a single secret
|
|
76
|
+
secret = client.get_secret(project_id: 'proj_123', environment: 'production', key: 'DATABASE_URL')
|
|
77
|
+
puts secret.value
|
|
78
|
+
|
|
79
|
+
# Set a secret (creates or updates)
|
|
80
|
+
client.set_secret(
|
|
81
|
+
project_id: 'proj_123',
|
|
82
|
+
environment: 'production',
|
|
83
|
+
key: 'API_KEY',
|
|
84
|
+
value: 'sk_live_...'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Delete a secret
|
|
88
|
+
client.delete_secret(project_id: 'proj_123', environment: 'production', key: 'OLD_KEY')
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Bulk Import
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
result = client.bulk_import(
|
|
95
|
+
project_id: 'proj_123',
|
|
96
|
+
environment: 'development',
|
|
97
|
+
secrets: [
|
|
98
|
+
KeyEnv::BulkSecretItem.new(key: 'DATABASE_URL', value: 'postgres://localhost/mydb'),
|
|
99
|
+
KeyEnv::BulkSecretItem.new(key: 'REDIS_URL', value: 'redis://localhost:6379'),
|
|
100
|
+
{ 'key' => 'API_KEY', 'value' => 'sk_test_...' } # Also accepts hashes
|
|
101
|
+
],
|
|
102
|
+
overwrite: true
|
|
103
|
+
)
|
|
104
|
+
puts "Created: #{result.created}, Updated: #{result.updated}"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Generate .env File
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
env_content = client.generate_env_file(project_id: 'proj_123', environment: 'production')
|
|
111
|
+
File.write('.env', env_content)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### List Projects and Environments
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# List all projects
|
|
118
|
+
projects = client.list_projects
|
|
119
|
+
projects.each do |project|
|
|
120
|
+
puts "#{project.name} (#{project.id})"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get project with environments
|
|
124
|
+
project = client.get_project(project_id: 'proj_123')
|
|
125
|
+
project.environments.each do |env|
|
|
126
|
+
puts " - #{env.name}"
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Service Token Info
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Get current user or service token info
|
|
134
|
+
user = client.get_current_user
|
|
135
|
+
|
|
136
|
+
if user.auth_type == 'service_token'
|
|
137
|
+
# Service tokens can access multiple projects
|
|
138
|
+
puts "Projects: #{user.project_ids}"
|
|
139
|
+
puts "Scopes: #{user.scopes}"
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Error Handling
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
require 'keyenv'
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
secret = client.get_secret(project_id: 'proj_123', environment: 'production', key: 'MISSING_KEY')
|
|
150
|
+
rescue KeyEnv::NotFoundError => e
|
|
151
|
+
puts "Secret not found: #{e.message}"
|
|
152
|
+
rescue KeyEnv::AuthenticationError => e
|
|
153
|
+
puts "Authentication failed: #{e.message}"
|
|
154
|
+
rescue KeyEnv::Error => e
|
|
155
|
+
puts "Error #{e.status}: #{e.message}"
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Error Types
|
|
160
|
+
|
|
161
|
+
| Error | Description |
|
|
162
|
+
|-------|-------------|
|
|
163
|
+
| `KeyEnv::Error` | Base error class |
|
|
164
|
+
| `KeyEnv::AuthenticationError` | Authentication failed (401) |
|
|
165
|
+
| `KeyEnv::NotFoundError` | Resource not found (404) |
|
|
166
|
+
| `KeyEnv::ValidationError` | Invalid request (422) |
|
|
167
|
+
| `KeyEnv::RateLimitError` | Rate limit exceeded (429) |
|
|
168
|
+
| `KeyEnv::ConnectionError` | Network/connection error |
|
|
169
|
+
| `KeyEnv::TimeoutError` | Request timeout |
|
|
170
|
+
|
|
171
|
+
## Caching
|
|
172
|
+
|
|
173
|
+
For serverless environments or high-traffic applications, enable caching to reduce API calls:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# Cache secrets for 5 minutes
|
|
177
|
+
client = KeyEnv.new(token: 'your-token', cache_ttl: 300)
|
|
178
|
+
|
|
179
|
+
# Or use environment variable
|
|
180
|
+
ENV['KEYENV_CACHE_TTL'] = '300'
|
|
181
|
+
client = KeyEnv.new(token: 'your-token')
|
|
182
|
+
|
|
183
|
+
# Manually clear cache
|
|
184
|
+
client.clear_cache # Clear all
|
|
185
|
+
client.clear_cache(project_id: 'proj_123') # Clear project
|
|
186
|
+
client.clear_cache(project_id: 'proj_123', environment: 'production') # Clear specific
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### `KeyEnv.new(token:, timeout:, cache_ttl:)`
|
|
192
|
+
|
|
193
|
+
Create a new KeyEnv client.
|
|
194
|
+
|
|
195
|
+
| Parameter | Type | Required | Default | Description |
|
|
196
|
+
|-----------|------|----------|---------|-------------|
|
|
197
|
+
| `token` | `String` | Yes | - | Service token |
|
|
198
|
+
| `timeout` | `Integer` | No | `30` | Request timeout (seconds) |
|
|
199
|
+
| `cache_ttl` | `Integer` | No | `0` | Cache TTL (seconds, 0 = disabled) |
|
|
200
|
+
|
|
201
|
+
### Methods
|
|
202
|
+
|
|
203
|
+
| Method | Description |
|
|
204
|
+
|--------|-------------|
|
|
205
|
+
| `get_current_user` | Get current user/token info |
|
|
206
|
+
| `list_projects` | List all accessible projects |
|
|
207
|
+
| `get_project(project_id:)` | Get project with environments |
|
|
208
|
+
| `list_environments(project_id:)` | List environments in a project |
|
|
209
|
+
| `list_secrets(project_id:, environment:)` | List secret keys (no values) |
|
|
210
|
+
| `export_secrets(project_id:, environment:)` | Export secrets with values |
|
|
211
|
+
| `export_secrets_as_hash(project_id:, environment:)` | Export as hash |
|
|
212
|
+
| `get_secret(project_id:, environment:, key:)` | Get single secret |
|
|
213
|
+
| `set_secret(project_id:, environment:, key:, value:)` | Create or update secret |
|
|
214
|
+
| `delete_secret(project_id:, environment:, key:)` | Delete secret |
|
|
215
|
+
| `bulk_import(project_id:, environment:, secrets:)` | Bulk import secrets |
|
|
216
|
+
| `load_env(project_id:, environment:)` | Load secrets into ENV |
|
|
217
|
+
| `generate_env_file(project_id:, environment:)` | Generate .env file content |
|
|
218
|
+
| `list_permissions(project_id:, environment:)` | List permissions |
|
|
219
|
+
| `set_permission(project_id:, environment:, user_id:, role:)` | Set permission |
|
|
220
|
+
| `delete_permission(project_id:, environment:, user_id:)` | Delete permission |
|
|
221
|
+
| `get_my_permissions(project_id:)` | Get current user's permissions |
|
|
222
|
+
|
|
223
|
+
## Requirements
|
|
224
|
+
|
|
225
|
+
- Ruby 3.0+
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Install dependencies
|
|
231
|
+
bundle install
|
|
232
|
+
|
|
233
|
+
# Run tests
|
|
234
|
+
bundle exec rspec
|
|
235
|
+
|
|
236
|
+
# Run linter
|
|
237
|
+
bundle exec rubocop
|
|
238
|
+
|
|
239
|
+
# Generate documentation
|
|
240
|
+
bundle exec rake yard
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module KeyEnv
|
|
9
|
+
# KeyEnv API client for managing secrets.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# client = KeyEnv::Client.new(token: "your-service-token")
|
|
13
|
+
# secrets = client.export_secrets(project_id: "proj_123", environment: "production")
|
|
14
|
+
#
|
|
15
|
+
# @example With caching for serverless environments
|
|
16
|
+
# client = KeyEnv::Client.new(token: "your-token", cache_ttl: 300) # 5 minutes
|
|
17
|
+
#
|
|
18
|
+
class Client
|
|
19
|
+
BASE_URL = "https://api.keyenv.dev"
|
|
20
|
+
DEFAULT_TIMEOUT = 30
|
|
21
|
+
|
|
22
|
+
# @param token [String] Service token for authentication
|
|
23
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
24
|
+
# @param cache_ttl [Integer] Cache TTL in seconds (default: 0 = disabled)
|
|
25
|
+
# @param base_url [String, nil] Custom API base URL (default: https://api.keyenv.dev)
|
|
26
|
+
def initialize(token:, timeout: DEFAULT_TIMEOUT, cache_ttl: 0, base_url: nil)
|
|
27
|
+
raise ArgumentError, "KeyEnv token is required" if token.nil? || token.empty?
|
|
28
|
+
|
|
29
|
+
@token = token
|
|
30
|
+
@timeout = timeout
|
|
31
|
+
@cache_ttl = cache_ttl.positive? ? cache_ttl : ENV.fetch("KEYENV_CACHE_TTL", "0").to_i
|
|
32
|
+
@base_url = base_url || ENV.fetch("KEYENV_API_URL", BASE_URL)
|
|
33
|
+
@base_uri = URI.parse(@base_url)
|
|
34
|
+
@secrets_cache = {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# =========================================================================
|
|
38
|
+
# Authentication
|
|
39
|
+
# =========================================================================
|
|
40
|
+
|
|
41
|
+
# Get the current user or service token info.
|
|
42
|
+
#
|
|
43
|
+
# @return [User] Current user/token info
|
|
44
|
+
def get_current_user
|
|
45
|
+
data = request(:get, "/api/v1/users/me")
|
|
46
|
+
User.new(data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validate the token and return user info.
|
|
50
|
+
#
|
|
51
|
+
# @return [User] Current user/token info
|
|
52
|
+
def validate_token
|
|
53
|
+
get_current_user
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# =========================================================================
|
|
57
|
+
# Projects
|
|
58
|
+
# =========================================================================
|
|
59
|
+
|
|
60
|
+
# List all accessible projects.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Project>] List of projects
|
|
63
|
+
def list_projects
|
|
64
|
+
data = request(:get, "/api/v1/projects")
|
|
65
|
+
(data["projects"] || []).map { |p| Project.new(p) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get a project by ID.
|
|
69
|
+
#
|
|
70
|
+
# @param project_id [String] Project ID
|
|
71
|
+
# @return [ProjectWithEnvironments] Project with environments
|
|
72
|
+
def get_project(project_id:)
|
|
73
|
+
data = request(:get, "/api/v1/projects/#{project_id}")
|
|
74
|
+
ProjectWithEnvironments.new(data)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create a new project.
|
|
78
|
+
#
|
|
79
|
+
# @param team_id [String] Team ID
|
|
80
|
+
# @param name [String] Project name
|
|
81
|
+
# @return [Project] Created project
|
|
82
|
+
def create_project(team_id:, name:)
|
|
83
|
+
data = request(:post, "/api/v1/projects", { team_id: team_id, name: name })
|
|
84
|
+
Project.new(data)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Delete a project.
|
|
88
|
+
#
|
|
89
|
+
# @param project_id [String] Project ID
|
|
90
|
+
def delete_project(project_id:)
|
|
91
|
+
request(:delete, "/api/v1/projects/#{project_id}")
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# =========================================================================
|
|
96
|
+
# Environments
|
|
97
|
+
# =========================================================================
|
|
98
|
+
|
|
99
|
+
# List environments in a project.
|
|
100
|
+
#
|
|
101
|
+
# @param project_id [String] Project ID
|
|
102
|
+
# @return [Array<Environment>] List of environments
|
|
103
|
+
def list_environments(project_id:)
|
|
104
|
+
data = request(:get, "/api/v1/projects/#{project_id}/environments")
|
|
105
|
+
(data["environments"] || []).map { |e| Environment.new(e) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Create a new environment.
|
|
109
|
+
#
|
|
110
|
+
# @param project_id [String] Project ID
|
|
111
|
+
# @param name [String] Environment name
|
|
112
|
+
# @param inherits_from [String, nil] Parent environment to inherit from
|
|
113
|
+
# @return [Environment] Created environment
|
|
114
|
+
def create_environment(project_id:, name:, inherits_from: nil)
|
|
115
|
+
payload = { name: name }
|
|
116
|
+
payload[:inherits_from] = inherits_from if inherits_from
|
|
117
|
+
data = request(:post, "/api/v1/projects/#{project_id}/environments", payload)
|
|
118
|
+
Environment.new(data)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Delete an environment.
|
|
122
|
+
#
|
|
123
|
+
# @param project_id [String] Project ID
|
|
124
|
+
# @param environment [String] Environment name
|
|
125
|
+
def delete_environment(project_id:, environment:)
|
|
126
|
+
request(:delete, "/api/v1/projects/#{project_id}/environments/#{environment}")
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# =========================================================================
|
|
131
|
+
# Secrets
|
|
132
|
+
# =========================================================================
|
|
133
|
+
|
|
134
|
+
# List secrets in an environment (keys and metadata only).
|
|
135
|
+
#
|
|
136
|
+
# @param project_id [String] Project ID
|
|
137
|
+
# @param environment [String] Environment name
|
|
138
|
+
# @return [Array<Secret>] List of secrets
|
|
139
|
+
def list_secrets(project_id:, environment:)
|
|
140
|
+
data = request(:get, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets")
|
|
141
|
+
(data["secrets"] || []).map { |s| Secret.new(s) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Export all secrets with their decrypted values.
|
|
145
|
+
# Results are cached when cache_ttl > 0.
|
|
146
|
+
#
|
|
147
|
+
# @param project_id [String] Project ID
|
|
148
|
+
# @param environment [String] Environment name
|
|
149
|
+
# @return [Array<SecretWithValue>] List of secrets with values
|
|
150
|
+
def export_secrets(project_id:, environment:)
|
|
151
|
+
cache_key = "#{project_id}:#{environment}"
|
|
152
|
+
|
|
153
|
+
# Check cache if TTL > 0
|
|
154
|
+
if @cache_ttl.positive?
|
|
155
|
+
cached = @secrets_cache[cache_key]
|
|
156
|
+
if cached
|
|
157
|
+
secrets, expires_at = cached
|
|
158
|
+
return secrets if Time.now.to_f < expires_at
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
data = request(:get, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets/export")
|
|
163
|
+
secrets = (data["secrets"] || []).map { |s| SecretWithValue.new(s) }
|
|
164
|
+
|
|
165
|
+
# Store in cache if TTL > 0
|
|
166
|
+
if @cache_ttl.positive?
|
|
167
|
+
@secrets_cache[cache_key] = [secrets, Time.now.to_f + @cache_ttl]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
secrets
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Export secrets as a key-value hash.
|
|
174
|
+
#
|
|
175
|
+
# @param project_id [String] Project ID
|
|
176
|
+
# @param environment [String] Environment name
|
|
177
|
+
# @return [Hash<String, String>] Secrets as key-value pairs
|
|
178
|
+
def export_secrets_as_hash(project_id:, environment:)
|
|
179
|
+
secrets = export_secrets(project_id: project_id, environment: environment)
|
|
180
|
+
secrets.each_with_object({}) { |s, h| h[s.key] = s.value }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Get a single secret with its value.
|
|
184
|
+
#
|
|
185
|
+
# @param project_id [String] Project ID
|
|
186
|
+
# @param environment [String] Environment name
|
|
187
|
+
# @param key [String] Secret key
|
|
188
|
+
# @return [SecretWithValue] Secret with value
|
|
189
|
+
def get_secret(project_id:, environment:, key:)
|
|
190
|
+
data = request(:get, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets/#{key}")
|
|
191
|
+
SecretWithValue.new(data["secret"] || data)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Create a new secret.
|
|
195
|
+
#
|
|
196
|
+
# @param project_id [String] Project ID
|
|
197
|
+
# @param environment [String] Environment name
|
|
198
|
+
# @param key [String] Secret key
|
|
199
|
+
# @param value [String] Secret value
|
|
200
|
+
# @param description [String, nil] Optional description
|
|
201
|
+
# @return [Secret] Created secret
|
|
202
|
+
def create_secret(project_id:, environment:, key:, value:, description: nil)
|
|
203
|
+
payload = { key: key, value: value }
|
|
204
|
+
payload[:description] = description if description
|
|
205
|
+
data = request(:post, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets", payload)
|
|
206
|
+
clear_cache(project_id: project_id, environment: environment)
|
|
207
|
+
Secret.new(data["secret"] || data)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Update a secret's value.
|
|
211
|
+
#
|
|
212
|
+
# @param project_id [String] Project ID
|
|
213
|
+
# @param environment [String] Environment name
|
|
214
|
+
# @param key [String] Secret key
|
|
215
|
+
# @param value [String] New secret value
|
|
216
|
+
# @param description [String, nil] Optional new description
|
|
217
|
+
# @return [Secret] Updated secret
|
|
218
|
+
def update_secret(project_id:, environment:, key:, value:, description: nil)
|
|
219
|
+
payload = { value: value }
|
|
220
|
+
payload[:description] = description unless description.nil?
|
|
221
|
+
data = request(:put, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets/#{key}", payload)
|
|
222
|
+
clear_cache(project_id: project_id, environment: environment)
|
|
223
|
+
Secret.new(data["secret"] || data)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Set a secret (create or update).
|
|
227
|
+
#
|
|
228
|
+
# @param project_id [String] Project ID
|
|
229
|
+
# @param environment [String] Environment name
|
|
230
|
+
# @param key [String] Secret key
|
|
231
|
+
# @param value [String] Secret value
|
|
232
|
+
# @param description [String, nil] Optional description
|
|
233
|
+
# @return [Secret] Created or updated secret
|
|
234
|
+
def set_secret(project_id:, environment:, key:, value:, description: nil)
|
|
235
|
+
update_secret(project_id: project_id, environment: environment, key: key, value: value, description: description)
|
|
236
|
+
rescue NotFoundError
|
|
237
|
+
create_secret(project_id: project_id, environment: environment, key: key, value: value, description: description)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Delete a secret.
|
|
241
|
+
#
|
|
242
|
+
# @param project_id [String] Project ID
|
|
243
|
+
# @param environment [String] Environment name
|
|
244
|
+
# @param key [String] Secret key
|
|
245
|
+
def delete_secret(project_id:, environment:, key:)
|
|
246
|
+
request(:delete, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets/#{key}")
|
|
247
|
+
clear_cache(project_id: project_id, environment: environment)
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Get secret version history.
|
|
252
|
+
#
|
|
253
|
+
# @param project_id [String] Project ID
|
|
254
|
+
# @param environment [String] Environment name
|
|
255
|
+
# @param key [String] Secret key
|
|
256
|
+
# @return [Array<SecretHistory>] Secret version history
|
|
257
|
+
def get_secret_history(project_id:, environment:, key:)
|
|
258
|
+
data = request(:get, "/api/v1/projects/#{project_id}/environments/#{environment}/secrets/#{key}/history")
|
|
259
|
+
(data["history"] || []).map { |h| SecretHistory.new(h) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Bulk import secrets.
|
|
263
|
+
#
|
|
264
|
+
# @param project_id [String] Project ID
|
|
265
|
+
# @param environment [String] Environment name
|
|
266
|
+
# @param secrets [Array<BulkSecretItem, Hash>] List of secrets to import
|
|
267
|
+
# @param overwrite [Boolean] Whether to overwrite existing secrets
|
|
268
|
+
# @return [BulkImportResult] Import result
|
|
269
|
+
def bulk_import(project_id:, environment:, secrets:, overwrite: false)
|
|
270
|
+
secret_list = secrets.map { |s| s.is_a?(BulkSecretItem) ? s.to_h : s }
|
|
271
|
+
data = request(
|
|
272
|
+
:post,
|
|
273
|
+
"/api/v1/projects/#{project_id}/environments/#{environment}/secrets/bulk",
|
|
274
|
+
{ secrets: secret_list, overwrite: overwrite }
|
|
275
|
+
)
|
|
276
|
+
clear_cache(project_id: project_id, environment: environment)
|
|
277
|
+
BulkImportResult.new(data)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# =========================================================================
|
|
281
|
+
# Utilities
|
|
282
|
+
# =========================================================================
|
|
283
|
+
|
|
284
|
+
# Load secrets into ENV.
|
|
285
|
+
#
|
|
286
|
+
# @param project_id [String] Project ID
|
|
287
|
+
# @param environment [String] Environment name
|
|
288
|
+
# @return [Integer] Number of secrets loaded
|
|
289
|
+
def load_env(project_id:, environment:)
|
|
290
|
+
secrets = export_secrets(project_id: project_id, environment: environment)
|
|
291
|
+
secrets.each { |s| ENV[s.key] = s.value }
|
|
292
|
+
secrets.size
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Generate .env file content from secrets.
|
|
296
|
+
#
|
|
297
|
+
# @param project_id [String] Project ID
|
|
298
|
+
# @param environment [String] Environment name
|
|
299
|
+
# @return [String] .env file content
|
|
300
|
+
def generate_env_file(project_id:, environment:)
|
|
301
|
+
secrets = export_secrets(project_id: project_id, environment: environment)
|
|
302
|
+
lines = [
|
|
303
|
+
"# Generated by KeyEnv",
|
|
304
|
+
"# Environment: #{environment}",
|
|
305
|
+
"# Generated at: #{Time.now.utc.iso8601}",
|
|
306
|
+
""
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
secrets.each do |secret|
|
|
310
|
+
value = secret.value
|
|
311
|
+
if value.include?("\n") || value.include?('"') || value.include?("'") || value.include?(" ") || value.include?("$")
|
|
312
|
+
escaped = value.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", "\\n").gsub("$", "\\$")
|
|
313
|
+
lines << %(#{secret.key}="#{escaped}")
|
|
314
|
+
else
|
|
315
|
+
lines << "#{secret.key}=#{value}"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
lines.join("\n") + "\n"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Clear the secrets cache.
|
|
323
|
+
#
|
|
324
|
+
# @param project_id [String, nil] Clear cache for specific project (optional)
|
|
325
|
+
# @param environment [String, nil] Clear cache for specific environment (requires project_id)
|
|
326
|
+
def clear_cache(project_id: nil, environment: nil)
|
|
327
|
+
if project_id && environment
|
|
328
|
+
cache_key = "#{project_id}:#{environment}"
|
|
329
|
+
@secrets_cache.delete(cache_key)
|
|
330
|
+
elsif project_id
|
|
331
|
+
@secrets_cache.delete_if { |k, _| k.start_with?("#{project_id}:") }
|
|
332
|
+
else
|
|
333
|
+
@secrets_cache.clear
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# =========================================================================
|
|
338
|
+
# Environment Permissions
|
|
339
|
+
# =========================================================================
|
|
340
|
+
|
|
341
|
+
# List permissions for an environment.
|
|
342
|
+
#
|
|
343
|
+
# @param project_id [String] Project ID
|
|
344
|
+
# @param environment [String] Environment name
|
|
345
|
+
# @return [Array<EnvironmentPermission>] List of permissions
|
|
346
|
+
def list_permissions(project_id:, environment:)
|
|
347
|
+
data = request(:get, "/api/v1/projects/#{project_id}/environments/#{environment}/permissions")
|
|
348
|
+
(data["permissions"] || []).map { |p| EnvironmentPermission.new(p) }
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Set a user's permission for an environment.
|
|
352
|
+
#
|
|
353
|
+
# @param project_id [String] Project ID
|
|
354
|
+
# @param environment [String] Environment name
|
|
355
|
+
# @param user_id [String] User ID
|
|
356
|
+
# @param role [String] Role ("none", "read", "write", or "admin")
|
|
357
|
+
# @return [EnvironmentPermission] Created or updated permission
|
|
358
|
+
def set_permission(project_id:, environment:, user_id:, role:)
|
|
359
|
+
data = request(
|
|
360
|
+
:put,
|
|
361
|
+
"/api/v1/projects/#{project_id}/environments/#{environment}/permissions/#{user_id}",
|
|
362
|
+
{ role: role }
|
|
363
|
+
)
|
|
364
|
+
EnvironmentPermission.new(data)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Delete a user's permission for an environment.
|
|
368
|
+
#
|
|
369
|
+
# @param project_id [String] Project ID
|
|
370
|
+
# @param environment [String] Environment name
|
|
371
|
+
# @param user_id [String] User ID
|
|
372
|
+
def delete_permission(project_id:, environment:, user_id:)
|
|
373
|
+
request(:delete, "/api/v1/projects/#{project_id}/environments/#{environment}/permissions/#{user_id}")
|
|
374
|
+
nil
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Bulk set permissions for an environment.
|
|
378
|
+
#
|
|
379
|
+
# @param project_id [String] Project ID
|
|
380
|
+
# @param environment [String] Environment name
|
|
381
|
+
# @param permissions [Array<Hash>] List of permission hashes with "user_id" and "role" keys
|
|
382
|
+
# @return [Array<EnvironmentPermission>] Created or updated permissions
|
|
383
|
+
def bulk_set_permissions(project_id:, environment:, permissions:)
|
|
384
|
+
data = request(
|
|
385
|
+
:put,
|
|
386
|
+
"/api/v1/projects/#{project_id}/environments/#{environment}/permissions",
|
|
387
|
+
{ permissions: permissions }
|
|
388
|
+
)
|
|
389
|
+
(data["permissions"] || []).map { |p| EnvironmentPermission.new(p) }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Get current user's permissions for all environments in a project.
|
|
393
|
+
#
|
|
394
|
+
# @param project_id [String] Project ID
|
|
395
|
+
# @return [Array<(Array<MyPermission>, Boolean)>] Tuple of (permissions, is_team_admin)
|
|
396
|
+
def get_my_permissions(project_id:)
|
|
397
|
+
data = request(:get, "/api/v1/projects/#{project_id}/my-permissions")
|
|
398
|
+
permissions = (data["permissions"] || []).map { |p| MyPermission.new(p) }
|
|
399
|
+
is_team_admin = data["is_team_admin"] || false
|
|
400
|
+
[permissions, is_team_admin]
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Get default permissions for a project.
|
|
404
|
+
#
|
|
405
|
+
# @param project_id [String] Project ID
|
|
406
|
+
# @return [Array<ProjectDefault>] List of project defaults
|
|
407
|
+
def get_project_defaults(project_id:)
|
|
408
|
+
data = request(:get, "/api/v1/projects/#{project_id}/permissions/defaults")
|
|
409
|
+
(data["defaults"] || []).map { |d| ProjectDefault.new(d) }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Set default permissions for a project.
|
|
413
|
+
#
|
|
414
|
+
# @param project_id [String] Project ID
|
|
415
|
+
# @param defaults [Array<Hash>] List of default hashes with "environment_name" and "default_role" keys
|
|
416
|
+
# @return [Array<ProjectDefault>] Updated project defaults
|
|
417
|
+
def set_project_defaults(project_id:, defaults:)
|
|
418
|
+
data = request(
|
|
419
|
+
:put,
|
|
420
|
+
"/api/v1/projects/#{project_id}/permissions/defaults",
|
|
421
|
+
{ defaults: defaults }
|
|
422
|
+
)
|
|
423
|
+
(data["defaults"] || []).map { |d| ProjectDefault.new(d) }
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
private
|
|
427
|
+
|
|
428
|
+
def request(method, path, body = nil)
|
|
429
|
+
uri = URI.join(@base_url, path)
|
|
430
|
+
|
|
431
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
432
|
+
http.use_ssl = uri.scheme == "https"
|
|
433
|
+
http.open_timeout = @timeout
|
|
434
|
+
http.read_timeout = @timeout
|
|
435
|
+
|
|
436
|
+
request = build_request(method, uri, body)
|
|
437
|
+
response = http.request(request)
|
|
438
|
+
|
|
439
|
+
handle_response(response)
|
|
440
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
441
|
+
raise TimeoutError.new("Request timeout", status: 408)
|
|
442
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
|
443
|
+
raise ConnectionError.new(e.message, status: 0)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def build_request(method, uri, body)
|
|
447
|
+
request_class = case method
|
|
448
|
+
when :get then Net::HTTP::Get
|
|
449
|
+
when :post then Net::HTTP::Post
|
|
450
|
+
when :put then Net::HTTP::Put
|
|
451
|
+
when :delete then Net::HTTP::Delete
|
|
452
|
+
else raise ArgumentError, "Unknown HTTP method: #{method}"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
request = request_class.new(uri)
|
|
456
|
+
request["Authorization"] = "Bearer #{@token}"
|
|
457
|
+
request["Content-Type"] = "application/json"
|
|
458
|
+
request["User-Agent"] = "keyenv-ruby/#{VERSION}"
|
|
459
|
+
request.body = JSON.generate(body) if body
|
|
460
|
+
|
|
461
|
+
request
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def handle_response(response)
|
|
465
|
+
return nil if response.code == "204"
|
|
466
|
+
|
|
467
|
+
body = response.body
|
|
468
|
+
data = body && !body.empty? ? JSON.parse(body) : {}
|
|
469
|
+
|
|
470
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
471
|
+
status = response.code.to_i
|
|
472
|
+
message = data["error"] || "Unknown error"
|
|
473
|
+
code = data["code"]
|
|
474
|
+
details = data["details"]
|
|
475
|
+
|
|
476
|
+
error_class = case status
|
|
477
|
+
when 401 then AuthenticationError
|
|
478
|
+
when 404 then NotFoundError
|
|
479
|
+
when 422 then ValidationError
|
|
480
|
+
when 429 then RateLimitError
|
|
481
|
+
else Error
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
raise error_class.new(message, status: status, code: code, details: details)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
data
|
|
488
|
+
rescue JSON::ParserError
|
|
489
|
+
raise Error.new(response.body || "Unknown error", status: response.code.to_i)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
data/lib/keyenv/error.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeyEnv
|
|
4
|
+
# Base error class for KeyEnv API errors.
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
# @return [Integer] HTTP status code
|
|
7
|
+
attr_reader :status
|
|
8
|
+
|
|
9
|
+
# @return [String, nil] Error code from API
|
|
10
|
+
attr_reader :code
|
|
11
|
+
|
|
12
|
+
# @return [Hash, nil] Additional error details
|
|
13
|
+
attr_reader :details
|
|
14
|
+
|
|
15
|
+
# @return [String] Original error message
|
|
16
|
+
attr_reader :original_message
|
|
17
|
+
|
|
18
|
+
# @param message [String] Error message
|
|
19
|
+
# @param status [Integer] HTTP status code (default: 0)
|
|
20
|
+
# @param code [String, nil] Error code from API
|
|
21
|
+
# @param details [Hash, nil] Additional error details
|
|
22
|
+
def initialize(message, status: 0, code: nil, details: nil)
|
|
23
|
+
@original_message = message
|
|
24
|
+
@status = status
|
|
25
|
+
@code = code
|
|
26
|
+
@details = details || {}
|
|
27
|
+
super(message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
if status.positive?
|
|
32
|
+
"KeyEnvError(#{status}): #{@original_message}"
|
|
33
|
+
else
|
|
34
|
+
"KeyEnvError: #{@original_message}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raised when authentication fails.
|
|
40
|
+
class AuthenticationError < Error; end
|
|
41
|
+
|
|
42
|
+
# Raised when a resource is not found.
|
|
43
|
+
class NotFoundError < Error; end
|
|
44
|
+
|
|
45
|
+
# Raised when the request is invalid.
|
|
46
|
+
class ValidationError < Error; end
|
|
47
|
+
|
|
48
|
+
# Raised when rate limited.
|
|
49
|
+
class RateLimitError < Error; end
|
|
50
|
+
|
|
51
|
+
# Raised on network/connection errors.
|
|
52
|
+
class ConnectionError < Error; end
|
|
53
|
+
|
|
54
|
+
# Raised on request timeout.
|
|
55
|
+
class TimeoutError < Error; end
|
|
56
|
+
end
|
data/lib/keyenv/types.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeyEnv
|
|
4
|
+
# User or service token info.
|
|
5
|
+
class User
|
|
6
|
+
attr_reader :id, :email, :name, :clerk_id, :avatar_url, :auth_type,
|
|
7
|
+
:team_id, :project_ids, :scopes, :created_at
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@id = data["id"]
|
|
11
|
+
@email = data["email"]
|
|
12
|
+
@name = data["name"]
|
|
13
|
+
@clerk_id = data["clerk_id"]
|
|
14
|
+
@avatar_url = data["avatar_url"]
|
|
15
|
+
@auth_type = data["auth_type"]
|
|
16
|
+
@team_id = data["team_id"]
|
|
17
|
+
@project_ids = data["project_ids"]
|
|
18
|
+
@scopes = data["scopes"]
|
|
19
|
+
@created_at = data["created_at"]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Project.
|
|
24
|
+
class Project
|
|
25
|
+
attr_reader :id, :team_id, :name, :slug, :description, :created_at
|
|
26
|
+
|
|
27
|
+
def initialize(data)
|
|
28
|
+
@id = data["id"]
|
|
29
|
+
@team_id = data["team_id"]
|
|
30
|
+
@name = data["name"]
|
|
31
|
+
@slug = data["slug"]
|
|
32
|
+
@description = data["description"]
|
|
33
|
+
@created_at = data["created_at"]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Environment.
|
|
38
|
+
class Environment
|
|
39
|
+
attr_reader :id, :project_id, :name, :inherits_from, :created_at
|
|
40
|
+
|
|
41
|
+
def initialize(data)
|
|
42
|
+
@id = data["id"]
|
|
43
|
+
@project_id = data["project_id"]
|
|
44
|
+
@name = data["name"]
|
|
45
|
+
@inherits_from = data["inherits_from"]
|
|
46
|
+
@created_at = data["created_at"]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Project with environments.
|
|
51
|
+
class ProjectWithEnvironments < Project
|
|
52
|
+
attr_reader :environments
|
|
53
|
+
|
|
54
|
+
def initialize(data)
|
|
55
|
+
super(data)
|
|
56
|
+
envs = data["environments"] || []
|
|
57
|
+
@environments = envs.map { |e| Environment.new(e) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Secret (without value).
|
|
62
|
+
class Secret
|
|
63
|
+
attr_reader :id, :environment_id, :key, :type, :version,
|
|
64
|
+
:description, :created_at, :updated_at
|
|
65
|
+
|
|
66
|
+
def initialize(data)
|
|
67
|
+
@id = data["id"]
|
|
68
|
+
@environment_id = data["environment_id"]
|
|
69
|
+
@key = data["key"]
|
|
70
|
+
@type = data["type"] || "string"
|
|
71
|
+
@version = data["version"]
|
|
72
|
+
@description = data["description"]
|
|
73
|
+
@created_at = data["created_at"]
|
|
74
|
+
@updated_at = data["updated_at"]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Secret with decrypted value.
|
|
79
|
+
class SecretWithValue < Secret
|
|
80
|
+
attr_reader :value, :inherited_from
|
|
81
|
+
|
|
82
|
+
def initialize(data)
|
|
83
|
+
super(data)
|
|
84
|
+
@value = data["value"] || ""
|
|
85
|
+
@inherited_from = data["inherited_from"]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Secret history entry.
|
|
90
|
+
class SecretHistory
|
|
91
|
+
attr_reader :id, :secret_id, :value, :version, :changed_by, :changed_at
|
|
92
|
+
|
|
93
|
+
def initialize(data)
|
|
94
|
+
@id = data["id"]
|
|
95
|
+
@secret_id = data["secret_id"]
|
|
96
|
+
@value = data["value"]
|
|
97
|
+
@version = data["version"]
|
|
98
|
+
@changed_by = data["changed_by"]
|
|
99
|
+
@changed_at = data["changed_at"]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Bulk import request item.
|
|
104
|
+
class BulkSecretItem
|
|
105
|
+
attr_reader :key, :value, :description
|
|
106
|
+
|
|
107
|
+
def initialize(key:, value:, description: nil)
|
|
108
|
+
@key = key
|
|
109
|
+
@value = value
|
|
110
|
+
@description = description
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_h
|
|
114
|
+
h = { "key" => key, "value" => value }
|
|
115
|
+
h["description"] = description if description
|
|
116
|
+
h
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Bulk import result.
|
|
121
|
+
class BulkImportResult
|
|
122
|
+
attr_reader :created, :updated, :skipped
|
|
123
|
+
|
|
124
|
+
def initialize(data)
|
|
125
|
+
@created = data["created"] || 0
|
|
126
|
+
@updated = data["updated"] || 0
|
|
127
|
+
@skipped = data["skipped"] || 0
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Environment permission for a user.
|
|
132
|
+
class EnvironmentPermission
|
|
133
|
+
attr_reader :id, :environment_id, :user_id, :role, :user_email,
|
|
134
|
+
:user_name, :granted_by, :created_at, :updated_at
|
|
135
|
+
|
|
136
|
+
def initialize(data)
|
|
137
|
+
@id = data["id"]
|
|
138
|
+
@environment_id = data["environment_id"]
|
|
139
|
+
@user_id = data["user_id"]
|
|
140
|
+
@role = data["role"]
|
|
141
|
+
@user_email = data["user_email"]
|
|
142
|
+
@user_name = data["user_name"]
|
|
143
|
+
@granted_by = data["granted_by"]
|
|
144
|
+
@created_at = data["created_at"] || ""
|
|
145
|
+
@updated_at = data["updated_at"] || ""
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# User's own permission for an environment.
|
|
150
|
+
class MyPermission
|
|
151
|
+
attr_reader :environment_id, :environment_name, :role,
|
|
152
|
+
:can_read, :can_write, :can_admin
|
|
153
|
+
|
|
154
|
+
def initialize(data)
|
|
155
|
+
@environment_id = data["environment_id"]
|
|
156
|
+
@environment_name = data["environment_name"]
|
|
157
|
+
@role = data["role"]
|
|
158
|
+
@can_read = data["can_read"]
|
|
159
|
+
@can_write = data["can_write"]
|
|
160
|
+
@can_admin = data["can_admin"]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Default permission for an environment in a project.
|
|
165
|
+
class ProjectDefault
|
|
166
|
+
attr_reader :id, :project_id, :environment_name, :default_role, :created_at
|
|
167
|
+
|
|
168
|
+
def initialize(data)
|
|
169
|
+
@id = data["id"]
|
|
170
|
+
@project_id = data["project_id"]
|
|
171
|
+
@environment_name = data["environment_name"]
|
|
172
|
+
@default_role = data["default_role"]
|
|
173
|
+
@created_at = data["created_at"] || ""
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/keyenv.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# KeyEnv Ruby SDK - Secure secrets management for development teams.
|
|
4
|
+
|
|
5
|
+
require_relative "keyenv/version"
|
|
6
|
+
require_relative "keyenv/error"
|
|
7
|
+
require_relative "keyenv/types"
|
|
8
|
+
require_relative "keyenv/client"
|
|
9
|
+
|
|
10
|
+
module KeyEnv
|
|
11
|
+
class << self
|
|
12
|
+
# Create a new KeyEnv client.
|
|
13
|
+
#
|
|
14
|
+
# @param token [String] Service token for authentication
|
|
15
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
16
|
+
# @param cache_ttl [Integer] Cache TTL in seconds (default: 0 = disabled)
|
|
17
|
+
# @param base_url [String, nil] Custom API base URL (default: https://api.keyenv.dev)
|
|
18
|
+
# @return [Client] KeyEnv client instance
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# client = KeyEnv.new(token: "your-service-token")
|
|
22
|
+
# secrets = client.export_secrets(project_id: "proj_123", environment: "production")
|
|
23
|
+
#
|
|
24
|
+
def new(token:, timeout: 30, cache_ttl: 0, base_url: nil)
|
|
25
|
+
Client.new(token: token, timeout: timeout, cache_ttl: cache_ttl, base_url: base_url)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a new KeyEnv client (alternative syntax).
|
|
29
|
+
#
|
|
30
|
+
# @param token [String] Service token for authentication
|
|
31
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
32
|
+
# @param cache_ttl [Integer] Cache TTL in seconds (default: 0 = disabled)
|
|
33
|
+
# @param base_url [String, nil] Custom API base URL (default: https://api.keyenv.dev)
|
|
34
|
+
# @return [Client] KeyEnv client instance
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# client = KeyEnv.create("your-service-token")
|
|
38
|
+
# secret = client.get_secret(project_id: "proj_123", environment: "production", key: "API_KEY")
|
|
39
|
+
#
|
|
40
|
+
def create(token, timeout: 30, cache_ttl: 0, base_url: nil)
|
|
41
|
+
Client.new(token: token, timeout: timeout, cache_ttl: cache_ttl, base_url: base_url)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: keyenv
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- KeyEnv
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
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: webmock
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.19'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.19'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.57'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.57'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop-rspec
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '2.25'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '2.25'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: yard
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.9'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0.9'
|
|
111
|
+
description: KeyEnv Ruby SDK provides a simple interface for managing secrets in your
|
|
112
|
+
Ruby applications. Fetch, create, and manage environment variables securely.
|
|
113
|
+
email:
|
|
114
|
+
- support@keyenv.dev
|
|
115
|
+
executables: []
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- CHANGELOG.md
|
|
120
|
+
- LICENSE
|
|
121
|
+
- README.md
|
|
122
|
+
- lib/keyenv.rb
|
|
123
|
+
- lib/keyenv/client.rb
|
|
124
|
+
- lib/keyenv/error.rb
|
|
125
|
+
- lib/keyenv/types.rb
|
|
126
|
+
- lib/keyenv/version.rb
|
|
127
|
+
homepage: https://keyenv.dev
|
|
128
|
+
licenses:
|
|
129
|
+
- MIT
|
|
130
|
+
metadata:
|
|
131
|
+
homepage_uri: https://keyenv.dev
|
|
132
|
+
source_code_uri: https://github.com/keyenv/ruby-sdk
|
|
133
|
+
changelog_uri: https://github.com/keyenv/ruby-sdk/blob/main/CHANGELOG.md
|
|
134
|
+
documentation_uri: https://keyenv.dev/docs/sdks/ruby
|
|
135
|
+
rubygems_mfa_required: 'true'
|
|
136
|
+
post_install_message:
|
|
137
|
+
rdoc_options: []
|
|
138
|
+
require_paths:
|
|
139
|
+
- lib
|
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: 3.0.0
|
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - ">="
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: '0'
|
|
150
|
+
requirements: []
|
|
151
|
+
rubygems_version: 3.0.3.1
|
|
152
|
+
signing_key:
|
|
153
|
+
specification_version: 4
|
|
154
|
+
summary: Official Ruby SDK for KeyEnv - Secure secrets management
|
|
155
|
+
test_files: []
|