opdotenv 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 484498d0c2471d83e66fd42819feef80da6432e79c2166ceb15b1ece037b2046
4
+ data.tar.gz: fce7dbd63416abb37af84c4bdf78792d1172fa1bea972d467f7a871ca1d58e12
5
+ SHA512:
6
+ metadata.gz: 319700ee96600edbb66bc90db27dadb07cfe269cc0f10570b582ae349f8f6234ff29a5950590a57763450c48a95ab1fcd3925bf0d929a1604d2e15a5cc335b11
7
+ data.tar.gz: 351b895887435dabeb2dec23884d741f192a16aa3c26be083490ff9a7be8189b06d03d8c8f0e7e3373a875d3d8edda5aabc620e5b2a157f05390293fb5cfbaf9
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # CHANGELOG
2
+
3
+ ## 1.0.0 (2025-11-04)
4
+
5
+ - Initial stable release
6
+ - Load environment variables from 1Password using op CLI or Connect API
7
+ - Support for dotenv, JSON, and YAML formats
8
+ - Automatic format inference from file extensions
9
+ - Rails integration with declarative configuration
10
+ - Anyway Config integration with strict prefix matching
11
+ - Export data back to 1Password
12
+ - CLI tool for read/export operations
13
+ - Support for field names in paths (e.g., op://Vault/Item/config.json)
14
+ - Security improvements (input validation, safe YAML parsing)
15
+ - Comprehensive documentation with security guidelines
data/LICENSE.md ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
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.
22
+
23
+
data/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # opdotenv
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/opdotenv.svg)](https://badge.fury.io/rb/opdotenv) [![Test Status](https://github.com/amkisko/opdotenv/actions/workflows/ci.yml/badge.svg)](https://github.com/amkisko/opdotenv/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/amkisko/opdotenv/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/amkisko/opdotenv/graph/badge.svg?token=YOUR_TOKEN)
4
+
5
+ Load environment variables from 1Password using the `op` CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.
6
+
7
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
8
+
9
+ ## Installation
10
+
11
+ Add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem "opdotenv"
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ Choose one:
20
+ - **1Password CLI** (`op`) - must be installed and authenticated (`op signin`)
21
+ - **1Password Connect Server** - set `OP_CONNECT_URL` and `OP_CONNECT_TOKEN` environment variables
22
+
23
+ Ruby 2.7+ supported.
24
+
25
+ ## Rails
26
+
27
+ Configure in `config/application.rb` or environment-specific files:
28
+
29
+ ```ruby
30
+ Rails.application.configure do
31
+ config.opdotenv.sources = [
32
+ "op://Vault/.env.development", # dotenv format (inferred)
33
+ "op://Vault/config.json", # json format (inferred from .json extension)
34
+ "op://Vault/App" # all fields without parsing
35
+ ]
36
+ end
37
+ ```
38
+
39
+ Format is automatically inferred from item name or field name:
40
+ - `.env.*` → dotenv format
41
+ - `*.json` → JSON format
42
+ - `*.yaml` or `*.yml` → YAML format
43
+ - Other items → load all fields without parsing
44
+
45
+ You can also specify the field name with extension in the path:
46
+ - `op://Vault/Item Name/config.json` → uses field `config.json` as JSON
47
+ - `op://Vault/Item Name/production.json` → uses field `production.json` as JSON
48
+ - `op://Vault/Item Name/.env.development` → uses field `.env.development` as dotenv
49
+
50
+ ### 1Password Connect
51
+
52
+ ```ruby
53
+ Rails.application.configure do
54
+ config.opdotenv.connect_url = "https://connect.example.com"
55
+ config.opdotenv.connect_token = Rails.application.credentials.dig(:op_connect, :token)
56
+ end
57
+ ```
58
+
59
+ ### Disable automatic loading
60
+
61
+ ```ruby
62
+ Rails.application.configure do
63
+ config.opdotenv.auto_load = false
64
+ end
65
+
66
+ # Load manually when needed
67
+ Opdotenv::Loader.load("op://Vault/Item")
68
+ ```
69
+
70
+ ## Standalone usage
71
+
72
+ ```ruby
73
+ require "opdotenv"
74
+
75
+ # Load from dotenv format (format inferred from item name)
76
+ Opdotenv::Loader.load("op://Vault/.env.development")
77
+
78
+ # Load from JSON format (any item name ending with .json)
79
+ Opdotenv::Loader.load("op://Vault/config.json")
80
+ Opdotenv::Loader.load("op://Vault/production.json")
81
+
82
+ # Load from field with extension in path
83
+ Opdotenv::Loader.load("op://Vault/Item Name/config.json")
84
+
85
+ # Load all fields from an item
86
+ Opdotenv::Loader.load("op://Vault/App")
87
+
88
+ # Don't overwrite existing ENV values
89
+ Opdotenv::Loader.load("op://Vault/Item", overwrite: false)
90
+ ```
91
+
92
+ ## Anyway Config integration
93
+
94
+ Automatically registers when `anyway_config` is available:
95
+
96
+ ```ruby
97
+ class AppConfig < Anyway::Config
98
+ attr_config :api_key, :api_secret
99
+
100
+ # Format is inferred from item name
101
+ loader_options opdotenv: {
102
+ path: "op://Vault/.env.development" # dotenv format inferred
103
+ }
104
+ end
105
+
106
+ # Or load all fields from an item
107
+ class DatabaseConfig < Anyway::Config
108
+ attr_config :url, :username, :password
109
+
110
+ loader_options opdotenv: {
111
+ path: "op://Vault/Database" # all fields loaded
112
+ }
113
+ end
114
+ ```
115
+
116
+ ### Conditional Loading (Recommended for Security)
117
+
118
+ For better security, only load from 1Password in development/test environments:
119
+
120
+ ```ruby
121
+ class TestConfig < Anyway::Config
122
+ config_name :test
123
+ attr_config :enabled, :sample
124
+
125
+ # Only load from 1Password in local/development environments
126
+ if Rails.env.local?
127
+ loader_options opdotenv: {
128
+ path: "op://Employee/.env.test"
129
+ }
130
+ end
131
+ end
132
+ ```
133
+
134
+ This ensures that production environments won't attempt to load secrets from 1Password, aligning with the [production recommendations](#environment-recommendation).
135
+
136
+ ### Loading All Fields from an Item
137
+
138
+ When loading all fields from a 1Password item (not a parsed format), field names are automatically normalized to match the `env_prefix`:
139
+
140
+ ```ruby
141
+ class TestConfig < Anyway::Config
142
+ config_name :test
143
+ attr_config :enabled, :sample
144
+
145
+ loader_options opdotenv: {
146
+ path: "op://Employee/TestConfig" # Loads all fields from item
147
+ }
148
+ end
149
+ ```
150
+
151
+ **Field name matching (strict with case-insensitive prefix):**
152
+ - Fields in 1Password **must** be prefixed with the `env_prefix` (e.g., `TEST_` for `config_name :test`)
153
+ - Matching is **case-insensitive**: `TEST_ENABLED`, `test_enabled`, `Test_Enabled` all work
154
+ - After prefix stripping, `TEST_ENABLED` becomes `enabled` (matching `attr_config :enabled`)
155
+ - Fields without the prefix (e.g., `enabled`, `ENABLED`) are ignored and logged as unmatched
156
+
157
+ **Debugging field matching:**
158
+ - Enable debug logging by setting `OPDOTENV_DEBUG=true`
159
+ - Check Rails logs for messages like:
160
+ ```
161
+ [opdotenv] Available fields from 1Password: enabled, ENABLED, sample, SAMPLE
162
+ [opdotenv] Matched fields for TEST: enabled, sample
163
+ [opdotenv] Unmatched fields (must be prefixed with TEST_, case-insensitive): other_field
164
+ ```
165
+
166
+ ## Using with dotenv
167
+
168
+ Load order determines which values take precedence:
169
+
170
+ ```ruby
171
+ require "dotenv"
172
+ require "opdotenv"
173
+
174
+ # Load local files first, then augment from 1Password (1Password values override by default)
175
+ Dotenv.load(".env", ".env.development")
176
+ Opdotenv::Loader.load("op://Vault/.env.development")
177
+
178
+ # Or load from 1Password first, then local files (local values override)
179
+ Opdotenv::Loader.load("op://Vault/.env.development", overwrite: false)
180
+ Dotenv.load(".env", ".env.development")
181
+ ```
182
+
183
+ ## Export to 1Password
184
+
185
+ ### CLI
186
+
187
+ ```bash
188
+ # Export .env file (format inferred from path)
189
+ opdotenv export --path "op://Vault/.env.development" --file .env.development
190
+
191
+ # Export to item fields
192
+ opdotenv export --path "op://Vault/App" --file .env
193
+
194
+ # Read and print (format inferred from path)
195
+ opdotenv read --path "op://Vault/.env.development"
196
+ ```
197
+
198
+ ### Ruby API
199
+
200
+ ```ruby
201
+ # Export to Secure Note (format inferred from path)
202
+ Opdotenv::Exporter.export(
203
+ path: "op://Vault/.env.development",
204
+ data: {"API_KEY" => "secret"}
205
+ )
206
+
207
+ # Export to item fields
208
+ Opdotenv::Exporter.export(
209
+ path: "op://Vault/App",
210
+ data: {"API_KEY" => "secret", "API_SECRET" => "another"}
211
+ )
212
+ ```
213
+
214
+ ## Supported formats
215
+
216
+ Format is automatically inferred from item name or field name:
217
+ - `.env.*` → dotenv format (`KEY=VALUE`)
218
+ - `*.json` → JSON format (nested structures flattened with underscores)
219
+ - `*.yaml` or `*.yml` → YAML format (nested structures flattened with underscores)
220
+ - Other items → load all fields without parsing
221
+
222
+ Field names can be specified with extensions in the path:
223
+ - `op://Vault/Item Name/config.json` → loads field `config.json` as JSON
224
+ - `op://Vault/Item Name/production.json` → loads field `production.json` as JSON
225
+ - `op://Vault/Item Name/.env.development` → loads field `.env.development` as dotenv
226
+
227
+ For advanced usage, you can explicitly specify `field_name` and `field_type` in the API.
228
+
229
+ ## Security
230
+
231
+ ### Environment Recommendation
232
+
233
+ **⚠️ This gem is recommended for development and test environments only.**
234
+
235
+ For production environments, we recommend using dedicated secret management solutions that integrate with your infrastructure:
236
+
237
+ - **Kubernetes**: Use [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) or [External Secrets Operator](https://external-secrets.io/) with providers like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault
238
+ - **AWS**: Use [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
239
+ - **Azure**: Use [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault)
240
+ - **GCP**: Use [Google Secret Manager](https://cloud.google.com/secret-manager)
241
+
242
+ Provision secrets through infrastructure-as-code tools:
243
+ - **Helm** (Kubernetes): Use `helm secrets` or external secrets operator
244
+ - **Terraform**: Use `aws_secretsmanager_secret`, `azurerm_key_vault_secret`, or `google_secret_manager_secret`
245
+ - **Bicep** (Azure): Use `Microsoft.KeyVault/vaults` resources
246
+
247
+ These solutions provide:
248
+ - Better audit trails and access controls
249
+ - Integration with IAM/RBAC systems
250
+ - Automatic secret rotation
251
+ - Compliance with security standards
252
+ - No dependency on external CLI tools or API servers
253
+
254
+ ### Security Considerations
255
+
256
+ - **CLI mode**: Secrets fetched via authenticated `op` CLI session.
257
+ - **Connect API mode**: Secrets fetched via HTTPS. Ensure tokens are secure.
258
+ - The library does not persist secrets in memory or on disk.
259
+ - Always verify 1Password CLI or Connect server is up to date and authenticated.
260
+
261
+ ### Code Locations for Security Review
262
+
263
+ For security-sensitive applications, developers should review where this gem reads and writes data from 1Password. The following locations handle secret data:
264
+
265
+ #### Reading Secrets (OpClient - CLI mode)
266
+
267
+ - **`lib/opdotenv/op_client.rb`**:
268
+ - `read(path)` - Executes `op read` command to fetch a single field value
269
+ - `item_get(item, vault:)` - Executes `op item get` command to fetch all item data as JSON
270
+ - `capture(args)` - Executes shell commands via `IO.popen` (array arguments, no shell interpretation)
271
+
272
+ #### Reading Secrets (ConnectApiClient - API mode)
273
+
274
+ - **`lib/opdotenv/connect_api_client.rb`**:
275
+ - `read(path)` - Makes HTTP GET request to fetch field or notesPlain content
276
+ - `item_get(item_title, vault:)` - Searches and fetches item data via API
277
+ - `get_item(vault_name, item_title)` - Fetches full item details including all fields
278
+ - `item_by_title_in_vault(vault_id, item_title)` - Lists items and fetches by title
279
+ - `list_vaults()` - Lists all accessible vaults (uses `api_request(:get, "/v1/vaults")`)
280
+ - `vault_name_to_id(vault_name)` - Resolves vault names to IDs (cached)
281
+ - `api_request(method, path, body)` - All HTTP requests go through this method
282
+ - `find_field(item, field_name)` - Searches item fields by label, ID, or purpose
283
+
284
+ #### Main Entry Points
285
+
286
+ - **`lib/opdotenv/loader.rb`**:
287
+ - `load(path, ...)` - Main entry point that orchestrates secret fetching
288
+ - `load_field(client, path, field_name, field_type)` - Loads and parses a single field
289
+ - `load_all_fields(client, path)` - Loads all fields from an item (skips notesPlain)
290
+ - `merge_into_env(env, hash, overwrite:)` - Writes secrets to environment hash
291
+
292
+ #### Rails Integration
293
+
294
+ - **`lib/opdotenv/railtie.rb`**:
295
+ - `initializer "opdotenv.load"` - Automatically loads secrets during Rails initialization
296
+ - Reads from `config.opdotenv.sources` array
297
+ - Sets `OP_CONNECT_URL` and `OP_CONNECT_TOKEN` from Rails config if provided
298
+
299
+ #### Anyway Config Integration
300
+
301
+ - **`lib/opdotenv/anyway_loader.rb`**:
302
+ - `Loader#call(...)` - Loads secrets for Anyway Config classes
303
+ - Uses `Opdotenv::Loader.load()` internally
304
+
305
+ #### Parsing and Processing
306
+
307
+ - **`lib/opdotenv/parsers/dotenv_parser.rb`** - Parses dotenv format strings
308
+ - **`lib/opdotenv/parsers/json_parser.rb`** - Parses and flattens JSON structures
309
+ - **`lib/opdotenv/parsers/yaml_parser.rb`** - Parses YAML (safe_load with aliases: false)
310
+
311
+ #### Data Flow
312
+
313
+ 1. **Configuration** → Rails config or direct API calls
314
+ 2. **Path Parsing** → `SourceParser.parse()` extracts vault/item/field from path
315
+ 3. **Client Selection** → `ClientFactory.create()` chooses CLI or API client
316
+ 4. **Secret Fetching** → Client reads from 1Password (CLI command or HTTP request)
317
+ 5. **Parsing** → Format-specific parser converts to key-value pairs
318
+ 6. **Environment Merge** → Secrets merged into `ENV` or provided hash
319
+
320
+ All secret data flows through these code paths. No secrets are persisted to disk or logged (except explicit error messages).
321
+
322
+ ## Development
323
+
324
+ ```bash
325
+ bundle install
326
+ bundle exec rspec
327
+ bundle exec rbs validate
328
+ bundle exec standardrb --fix
329
+ ```
330
+
331
+ ## License
332
+
333
+ MIT
data/bin/opdotenv ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "opdotenv"
6
+ require "optparse"
7
+
8
+ cmd = ARGV.shift
9
+
10
+ case cmd
11
+ when "read"
12
+ path = nil
13
+ OptionParser.new do |o|
14
+ o.on("--path PATH", "op://Vault/Item") { |v| path = v }
15
+ end.parse!(ARGV)
16
+ abort("--path required") unless path
17
+ data = Opdotenv::Loader.load(path)
18
+ puts Opdotenv::Exporter.serialize_by_format(data, :dotenv)
19
+ when "export"
20
+ path = file = nil
21
+ field_type = nil
22
+ OptionParser.new do |o|
23
+ o.on("--path PATH", "op://Vault/Item") { |v| path = v }
24
+ o.on("--file PATH", "Source file (.env/json/yaml)") { |v| file = v }
25
+ o.on("--field-type TYPE", [:dotenv, :json, :yaml, :yml], "Override format (default: inferred from path)") { |v| field_type = v }
26
+ end.parse!(ARGV)
27
+ abort("--path and --file required") unless path && file
28
+ # Infer format from file extension if not provided
29
+ field_type ||= infer_format_from_file(file)
30
+ text = File.read(file)
31
+ data = Opdotenv::Loader.parse_by_format(text, field_type)
32
+ Opdotenv::Exporter.export(path: path, data: data, field_type: field_type)
33
+ else
34
+ warn "Usage: opdotenv (read|export) ..."
35
+ exit 1
36
+ end
37
+
38
+ def infer_format_from_file(file)
39
+ case File.extname(file)
40
+ when ".json" then :json
41
+ when ".yaml", ".yml" then :yaml
42
+ else :dotenv
43
+ end
44
+ end
@@ -0,0 +1,125 @@
1
+ module Opdotenv
2
+ # Anyway Config loader to fetch configuration from 1Password via opdotenv.
3
+ # Usage per-config:
4
+ # class MyConfig < Anyway::Config
5
+ # loader_options opdotenv: {
6
+ # path: "op://Vault/.env.development" # format inferred from item name
7
+ # }
8
+ # end
9
+ module AnywayLoader
10
+ class Loader < ::Anyway::Loaders::Base
11
+ def call(name:, env_prefix:, config_path:, **opts)
12
+ options = opts[:opdotenv] || {}
13
+ return {} if options.empty?
14
+
15
+ path = options[:path] || options["path"]
16
+ raise ArgumentError, "opdotenv loader requires :path" unless path
17
+
18
+ parsed = SourceParser.parse(path)
19
+ overwrite = options.key?(:overwrite) ? options[:overwrite] : true
20
+
21
+ # Use a separate env hash to avoid side effects on global ENV
22
+ data = Opdotenv::Loader.load(
23
+ path,
24
+ field_name: parsed[:field_name],
25
+ field_type: parsed[:field_type],
26
+ env: {},
27
+ overwrite: overwrite
28
+ )
29
+
30
+ strip_prefix_from_keys(data, env_prefix)
31
+ end
32
+
33
+ private
34
+
35
+ def strip_prefix_from_keys(data, env_prefix)
36
+ return data unless defined?(::Anyway::Env) && defined?(::Anyway::NoCast)
37
+
38
+ # Strict matching with case-insensitive prefix: Only fields with env_prefix will be matched
39
+ # Normalize keys to uppercase for case-insensitive matching
40
+ normalized_data = normalize_keys_for_prefix_matching(data, env_prefix)
41
+
42
+ # Use Anyway::Env to handle prefix stripping, matching how Doppler loader works
43
+ # This transforms keys like "TEST_ENABLED" -> "enabled" when env_prefix is "TEST"
44
+ env = ::Anyway::Env.new(type_cast: ::Anyway::NoCast, env_container: normalized_data)
45
+ conf, trace = env.fetch_with_trace(env_prefix)
46
+
47
+ if defined?(::Anyway::Tracing) && ::Anyway::Tracing.current_trace
48
+ ::Anyway::Tracing.current_trace.merge!(trace)
49
+ end
50
+
51
+ # Log available fields for debugging (only when OPDOTENV_DEBUG is enabled)
52
+ log_available_fields(data, env_prefix, conf)
53
+
54
+ conf
55
+ end
56
+
57
+ def normalize_keys_for_prefix_matching(data, env_prefix)
58
+ return data if data.empty? || env_prefix.empty?
59
+
60
+ prefix_upper = env_prefix.upcase
61
+ prefix_with_underscore = "#{prefix_upper}_"
62
+
63
+ normalized = {}
64
+ data.each do |key, value|
65
+ key_str = key.to_s
66
+ key_upper = key_str.upcase
67
+
68
+ # Case-insensitive prefix matching: only include keys that start with PREFIX_
69
+ if key_upper.start_with?(prefix_with_underscore) || key_upper == prefix_upper
70
+ # Normalize to uppercase for consistent matching
71
+ normalized_key = key_upper
72
+ normalized[normalized_key] = value
73
+ end
74
+ end
75
+
76
+ normalized
77
+ end
78
+
79
+ def log_available_fields(original_data, env_prefix, matched_data)
80
+ return unless ENV["OPDOTENV_DEBUG"] == "true"
81
+ return unless defined?(Rails) && Rails.logger
82
+
83
+ prefix_upper = env_prefix.upcase
84
+ prefix_with_underscore = "#{prefix_upper}_"
85
+
86
+ available_fields = original_data.keys.map(&:to_s)
87
+ matched_fields = matched_data.keys.map(&:to_s)
88
+
89
+ # Find fields that were available but didn't match the prefix (case-insensitive)
90
+ unmatched = available_fields.reject do |field|
91
+ field_upper = field.to_s.upcase
92
+ field_upper.start_with?(prefix_with_underscore) || field_upper == prefix_upper
93
+ end
94
+
95
+ if available_fields.any?
96
+ Rails.logger.debug("[opdotenv] Available fields from 1Password: #{available_fields.join(", ")}")
97
+ Rails.logger.debug("[opdotenv] Matched fields for #{env_prefix} (prefixed with #{prefix_with_underscore}, case-insensitive): #{matched_fields.join(", ")}")
98
+ if unmatched.any?
99
+ Rails.logger.debug("[opdotenv] Unmatched fields (must be prefixed with #{prefix_with_underscore}, case-insensitive): #{unmatched.join(", ")}")
100
+ Rails.logger.debug("[opdotenv] To use these fields, rename them in 1Password to include the #{prefix_with_underscore} prefix")
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def self.register!
107
+ ::Anyway.loaders.append :opdotenv, Loader
108
+ end
109
+ end
110
+ end
111
+
112
+ begin
113
+ # Auto-register if Anyway is available
114
+ # Silently skip if Anyway Config is not available or not fully loaded
115
+ if defined?(::Anyway) && ::Anyway.respond_to?(:loaders)
116
+ Opdotenv::AnywayLoader.register!
117
+ end
118
+ rescue => e
119
+ # Only warn if debugging is enabled, as this is expected when Anyway Config isn't used
120
+ if ENV["OPDOTENV_DEBUG"] == "true"
121
+ warn "[opdotenv] Failed to register Anyway loader: #{e.message}"
122
+ warn "[opdotenv] Error details: #{e.class}: #{e.message}"
123
+ warn "[opdotenv] Backtrace: #{e.backtrace.first(3).join("\n")}" if e.backtrace
124
+ end
125
+ end
@@ -0,0 +1,17 @@
1
+ module Opdotenv
2
+ class ClientFactory
3
+ # Creates appropriate client based on configuration or environment
4
+ # Supports both op CLI and Connect API
5
+ def self.create(env: ENV)
6
+ # Check for Connect API configuration
7
+ connect_url = env["OP_CONNECT_URL"] || env["OPDOTENV_CONNECT_URL"]
8
+ connect_token = env["OP_CONNECT_TOKEN"] || env["OPDOTENV_CONNECT_TOKEN"]
9
+
10
+ if connect_url && connect_token
11
+ ConnectApiClient.new(base_url: connect_url, access_token: connect_token, env: env)
12
+ else
13
+ OpClient.new(env: env)
14
+ end
15
+ end
16
+ end
17
+ end