opdotenv 1.0.3 → 1.0.6
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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +76 -68
- data/bin/opdotenv +1 -9
- data/lib/opdotenv/anyway_loader.rb +22 -10
- data/lib/opdotenv/connect_api_client.rb +59 -103
- data/lib/opdotenv/exporter.rb +0 -1
- data/lib/opdotenv/format_inferrer.rb +11 -4
- data/lib/opdotenv/loader.rb +14 -13
- data/lib/opdotenv/op_client.rb +37 -22
- data/lib/opdotenv/parsers/dotenv_parser.rb +26 -19
- data/lib/opdotenv/source_parser.rb +5 -6
- data/lib/opdotenv/version.rb +1 -1
- data/lib/opdotenv.rb +0 -1
- metadata +77 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9bbc6ae3316e89e48c9c52e3f3aa33f3a3572217eab08b88ddc98283989b7f04
|
|
4
|
+
data.tar.gz: e964c986cd6676f4a0f3d0c3c33f633b520c9f277b6030f79a407f0db3cd8fdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1c8b865089d2a6a2463f7ebc5edffd60566a826fb9667c792ef02c61c113a309a892adf2fbb5ade5115868fcc52b58919a15b79ecb3e84d3017531a6aebb678
|
|
7
|
+
data.tar.gz: 2e308264ed8c9aadbd846daef1c9804f98f8571cffd902be3fece3613b586d5240edffa4d0d2dffa2008573a3280e6994f5089635628a4bc1a4a6af1d70be2ff
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.0.6 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
- Keep exported secrets out of 1Password CLI command arguments.
|
|
6
|
+
- Upsert Secure Note exports on create and edit for both `op` CLI and Connect.
|
|
7
|
+
- Use dedicated configuration items for CLI exports; existing-item updates cannot preserve passkeys.
|
|
8
|
+
- Infer export formats correctly when no override is supplied; match `.env` basenames only.
|
|
9
|
+
- Accept successful Connect API creation responses and avoid repeating writes after timeouts.
|
|
10
|
+
- Filter Connect item lookups by title and omit server response values from error messages.
|
|
11
|
+
- Fetch item details once per read after the filtered list.
|
|
12
|
+
- Cancel release preparation when confirmation input is closed.
|
|
13
|
+
|
|
14
|
+
## 1.0.5 (2026-07-06)
|
|
15
|
+
|
|
16
|
+
- Improve `OPDOTENV_DEBUG` field summary logging in Anyway Config integration to build messages only when Rails debug logging is active
|
|
17
|
+
- Skip field summary debug output when a 1Password item has no fields
|
|
18
|
+
|
|
19
|
+
## 1.0.4 (2026-07-05)
|
|
20
|
+
|
|
21
|
+
- Move `railties` from runtime to development dependency in gemspec
|
|
22
|
+
- Non-Rails installs no longer pull in the Rails gem tree; Rails apps already provide `railties`
|
|
23
|
+
|
|
3
24
|
## 1.0.3 (2025-11-07)
|
|
4
25
|
|
|
5
26
|
- Fix exit code handling in OpClient by using Open3.capture2e instead of IO.popen for reliable process status
|
data/README.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
# opdotenv
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/opdotenv) [](https://github.com/amkisko/opdotenv.rb/actions/workflows/test.yml) [](https://app.codecov.io/github/amkisko/opdotenv.rb) [](https://sonarcloud.io/project/overview?id=amkisko_opdotenv.rb)
|
|
4
4
|
|
|
5
5
|
Load environment variables from 1Password using the `op` CLI or 1Password Connect Server API. Supports dotenv, JSON, and YAML formats.
|
|
6
6
|
|
|
7
|
-
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
8
|
-
|
|
9
|
-
<a href="https://www.kiskolabs.com">
|
|
10
|
-
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
11
|
-
</a>
|
|
12
|
-
|
|
13
7
|
## Installation
|
|
14
8
|
|
|
15
9
|
Add to your Gemfile:
|
|
@@ -26,7 +20,7 @@ Choose one:
|
|
|
26
20
|
- You can configure a custom path via `OP_CLI_PATH` or `OPDOTENV_CLI_PATH` environment variables, or via Rails config (see below)
|
|
27
21
|
- **1Password Connect Server** - set `OP_CONNECT_URL` and `OP_CONNECT_TOKEN` environment variables
|
|
28
22
|
|
|
29
|
-
Ruby
|
|
23
|
+
Ruby 3.4+ required.
|
|
30
24
|
|
|
31
25
|
## Rails
|
|
32
26
|
|
|
@@ -42,16 +36,7 @@ Rails.application.configure do
|
|
|
42
36
|
end
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
Format is
|
|
46
|
-
- `.env.*` → dotenv format
|
|
47
|
-
- `*.json` → JSON format
|
|
48
|
-
- `*.yaml` or `*.yml` → YAML format
|
|
49
|
-
- Other items → load all fields without parsing
|
|
50
|
-
|
|
51
|
-
You can also specify the field name with extension in the path:
|
|
52
|
-
- `op://Vault/Item Name/config.json` → uses field `config.json` as JSON
|
|
53
|
-
- `op://Vault/Item Name/production.json` → uses field `production.json` as JSON
|
|
54
|
-
- `op://Vault/Item Name/.env.development` → uses field `.env.development` as dotenv
|
|
39
|
+
Format is inferred from the item or field basename (see [Supported formats](#supported-formats)). Field names with extensions work in the path, for example `op://Vault/Item Name/config.json`.
|
|
55
40
|
|
|
56
41
|
### 1Password Connect
|
|
57
42
|
|
|
@@ -135,16 +120,15 @@ class DatabaseConfig < Anyway::Config
|
|
|
135
120
|
end
|
|
136
121
|
```
|
|
137
122
|
|
|
138
|
-
### Conditional
|
|
123
|
+
### Conditional loading
|
|
139
124
|
|
|
140
|
-
|
|
125
|
+
Load from 1Password only in local or development environments:
|
|
141
126
|
|
|
142
127
|
```ruby
|
|
143
128
|
class TestConfig < Anyway::Config
|
|
144
129
|
config_name :test
|
|
145
130
|
attr_config :enabled, :sample
|
|
146
131
|
|
|
147
|
-
# Only load from 1Password in local/development environments
|
|
148
132
|
if Rails.env.local?
|
|
149
133
|
loader_options opdotenv: {
|
|
150
134
|
path: "op://Employee/.env.test"
|
|
@@ -153,7 +137,7 @@ class TestConfig < Anyway::Config
|
|
|
153
137
|
end
|
|
154
138
|
```
|
|
155
139
|
|
|
156
|
-
|
|
140
|
+
See [Environment recommendation](#environment-recommendation).
|
|
157
141
|
|
|
158
142
|
### Loading All Fields from an Item
|
|
159
143
|
|
|
@@ -204,6 +188,8 @@ Dotenv.load(".env", ".env.development")
|
|
|
204
188
|
|
|
205
189
|
## Export to 1Password
|
|
206
190
|
|
|
191
|
+
Use dedicated configuration items for exports through the `op` CLI. Export values are sent through standard input to keep them out of process arguments. Updating an existing item uses a JSON template and does not support items containing passkeys: 1Password warns that template updates overwrite passkeys. Do not export into a login item that contains a passkey. See the [1Password item edit documentation](https://www.1password.dev/cli/reference/management-commands/item#edit-an-item-using-a-template).
|
|
192
|
+
|
|
207
193
|
### CLI
|
|
208
194
|
|
|
209
195
|
```bash
|
|
@@ -235,50 +221,33 @@ Opdotenv::Exporter.export(
|
|
|
235
221
|
|
|
236
222
|
## Supported formats
|
|
237
223
|
|
|
238
|
-
Format is
|
|
239
|
-
- `.env
|
|
240
|
-
- `*.json
|
|
241
|
-
- `*.yaml` or `*.yml
|
|
242
|
-
- Other items
|
|
243
|
-
|
|
244
|
-
Field names can be specified with extensions in the path:
|
|
245
|
-
- `op://Vault/Item Name/config.json` → loads field `config.json` as JSON
|
|
246
|
-
- `op://Vault/Item Name/production.json` → loads field `production.json` as JSON
|
|
247
|
-
- `op://Vault/Item Name/.env.development` → loads field `.env.development` as dotenv
|
|
224
|
+
Format is inferred from the item or field basename:
|
|
225
|
+
- `.env` or `.env.*`: dotenv (`KEY=VALUE`)
|
|
226
|
+
- `*.json`: JSON (nested keys flattened with underscores)
|
|
227
|
+
- `*.yaml` or `*.yml`: YAML (nested keys flattened with underscores)
|
|
228
|
+
- Other items: load all fields without parsing
|
|
248
229
|
|
|
249
|
-
|
|
230
|
+
Field names with extensions work in the path, for example `op://Vault/Item Name/config.json`. Callers can also pass `field_name` and `field_type` to the API.
|
|
250
231
|
|
|
251
232
|
## Security
|
|
252
233
|
|
|
253
|
-
### Environment
|
|
254
|
-
|
|
255
|
-
**⚠️ This gem is recommended for development and test environments only.**
|
|
256
|
-
|
|
257
|
-
For production environments, we recommend using dedicated secret management solutions that integrate with your infrastructure:
|
|
234
|
+
### Environment recommendation
|
|
258
235
|
|
|
259
|
-
|
|
260
|
-
- **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)
|
|
261
|
-
- **Azure**: Use [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault)
|
|
262
|
-
- **GCP**: Use [Google Secret Manager](https://cloud.google.com/secret-manager)
|
|
236
|
+
Use this gem in development and test. For production, prefer the secret store that matches the host:
|
|
263
237
|
|
|
264
|
-
|
|
265
|
-
- **
|
|
266
|
-
- **
|
|
267
|
-
- **
|
|
238
|
+
- **Kubernetes**: [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) or [External Secrets Operator](https://external-secrets.io/) with AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault
|
|
239
|
+
- **AWS**: [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)
|
|
240
|
+
- **Azure**: [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault)
|
|
241
|
+
- **GCP**: [Google Secret Manager](https://cloud.google.com/secret-manager)
|
|
268
242
|
|
|
269
|
-
|
|
270
|
-
- Better audit trails and access controls
|
|
271
|
-
- Integration with IAM/RBAC systems
|
|
272
|
-
- Automatic secret rotation
|
|
273
|
-
- Compliance with security standards
|
|
274
|
-
- No dependency on external CLI tools or API servers
|
|
243
|
+
Provision through infrastructure tools such as Helm, Terraform, or Bicep when that matches the host.
|
|
275
244
|
|
|
276
|
-
### Security
|
|
245
|
+
### Security considerations
|
|
277
246
|
|
|
278
|
-
- **CLI mode**: Secrets fetched via authenticated `op` CLI session.
|
|
279
|
-
- **Connect API mode**: Secrets fetched via HTTPS.
|
|
280
|
-
- The library does not persist secrets in memory or on disk.
|
|
281
|
-
-
|
|
247
|
+
- **CLI mode**: Secrets fetched via an authenticated `op` CLI session.
|
|
248
|
+
- **Connect API mode**: Secrets fetched via HTTPS. Keep tokens out of logs and process arguments.
|
|
249
|
+
- The library does not persist secrets in memory beyond the load, or on disk.
|
|
250
|
+
- Keep the 1Password CLI or Connect server current and authenticated.
|
|
282
251
|
|
|
283
252
|
### Code Locations for Security Review
|
|
284
253
|
|
|
@@ -289,7 +258,7 @@ For security-sensitive applications, developers should review where this gem rea
|
|
|
289
258
|
- **`lib/opdotenv/op_client.rb`**:
|
|
290
259
|
- `read(path)` - Executes `op read` command to fetch a single field value
|
|
291
260
|
- `item_get(item, vault:)` - Executes `op item get` command to fetch all item data as JSON
|
|
292
|
-
- `capture(args)` -
|
|
261
|
+
- `capture(args)` - Runs the CLI via `Open3.capture2e` (array arguments, optional stdin)
|
|
293
262
|
|
|
294
263
|
#### Reading Secrets (ConnectApiClient - API mode)
|
|
295
264
|
|
|
@@ -297,7 +266,7 @@ For security-sensitive applications, developers should review where this gem rea
|
|
|
297
266
|
- `read(path)` - Makes HTTP GET request to fetch field or notesPlain content
|
|
298
267
|
- `item_get(item_title, vault:)` - Searches and fetches item data via API
|
|
299
268
|
- `get_item(vault_name, item_title)` - Fetches full item details including all fields
|
|
300
|
-
- `item_by_title_in_vault(vault_id, item_title)` -
|
|
269
|
+
- `item_by_title_in_vault(vault_id, item_title)` - Filters items by title and fetches full details
|
|
301
270
|
- `list_vaults()` - Lists all accessible vaults (uses `api_request(:get, "/v1/vaults")`)
|
|
302
271
|
- `vault_name_to_id(vault_name)` - Resolves vault names to IDs (cached)
|
|
303
272
|
- `api_request(method, path, body)` - All HTTP requests go through this method
|
|
@@ -330,14 +299,14 @@ For security-sensitive applications, developers should review where this gem rea
|
|
|
330
299
|
- **`lib/opdotenv/parsers/json_parser.rb`** - Parses and flattens JSON structures
|
|
331
300
|
- **`lib/opdotenv/parsers/yaml_parser.rb`** - Parses YAML (safe_load with aliases: false)
|
|
332
301
|
|
|
333
|
-
#### Data
|
|
302
|
+
#### Data flow
|
|
334
303
|
|
|
335
|
-
1. **Configuration
|
|
336
|
-
2. **Path
|
|
337
|
-
3. **Client
|
|
338
|
-
4. **Secret
|
|
339
|
-
5. **Parsing
|
|
340
|
-
6. **Environment
|
|
304
|
+
1. **Configuration**: Rails config or direct API calls
|
|
305
|
+
2. **Path parsing**: `SourceParser.parse()` extracts vault/item/field from path
|
|
306
|
+
3. **Client selection**: `ClientFactory.create()` chooses CLI or API client
|
|
307
|
+
4. **Secret fetching**: Client reads from 1Password (CLI command or HTTP request)
|
|
308
|
+
5. **Parsing**: Format-specific parser converts to key-value pairs
|
|
309
|
+
6. **Environment merge**: Secrets merged into `ENV` or provided hash
|
|
341
310
|
|
|
342
311
|
All secret data flows through these code paths. No secrets are persisted to disk or logged (except explicit error messages).
|
|
343
312
|
|
|
@@ -345,11 +314,50 @@ All secret data flows through these code paths. No secrets are persisted to disk
|
|
|
345
314
|
|
|
346
315
|
```bash
|
|
347
316
|
bundle install
|
|
348
|
-
bundle exec rspec
|
|
317
|
+
bundle exec polyrun parallel-rspec --workers 5 --merge-failures
|
|
349
318
|
bundle exec rbs validate
|
|
350
319
|
bundle exec standardrb --fix
|
|
351
320
|
```
|
|
352
321
|
|
|
322
|
+
## Contributing
|
|
323
|
+
|
|
324
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/opdotenv.rb.
|
|
325
|
+
|
|
326
|
+
Contribution policy:
|
|
327
|
+
- New features are not necessarily added to the gem
|
|
328
|
+
- Pull request should have test coverage for affected parts
|
|
329
|
+
- Pull request should have changelog entry
|
|
330
|
+
|
|
331
|
+
Review policy:
|
|
332
|
+
- It might take up to 2 calendar weeks to review and merge critical fixes
|
|
333
|
+
- It might take up to 6 calendar months to review and merge pull request
|
|
334
|
+
- It might take up to 1 calendar year to review an issue
|
|
335
|
+
|
|
336
|
+
## Security
|
|
337
|
+
|
|
338
|
+
If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.
|
|
339
|
+
|
|
340
|
+
## Links
|
|
341
|
+
|
|
342
|
+
- [GitHub](https://github.com/amkisko/opdotenv.rb)
|
|
343
|
+
- [GitLab](https://gitlab.com/amkisko/opdotenv.rb)
|
|
344
|
+
- [RubyGems](https://rubygems.org/gems/opdotenv)
|
|
345
|
+
- [Versions Atom](https://rubygems.org/gems/opdotenv/versions.atom) (feed id `9869611634128`)
|
|
346
|
+
- [libraries.io](https://libraries.io/rubygems/opdotenv)
|
|
347
|
+
- [Deps.dev](https://deps.dev/rubygems/opdotenv)
|
|
348
|
+
- [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_opdotenv.rb)
|
|
349
|
+
- [Snyk](https://snyk.io/test/github/amkisko/opdotenv.rb)
|
|
350
|
+
- [Codecov](https://app.codecov.io/github/amkisko/opdotenv.rb)
|
|
351
|
+
- [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/opdotenv.rb)
|
|
352
|
+
|
|
353
353
|
## License
|
|
354
354
|
|
|
355
|
-
MIT
|
|
355
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
356
|
+
|
|
357
|
+
## Sponsors
|
|
358
|
+
|
|
359
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
360
|
+
|
|
361
|
+
<a href="https://www.kiskolabs.com">
|
|
362
|
+
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
363
|
+
</a>
|
data/bin/opdotenv
CHANGED
|
@@ -27,7 +27,7 @@ when "export"
|
|
|
27
27
|
end.parse!(ARGV)
|
|
28
28
|
abort("--path and --file required") unless path && file
|
|
29
29
|
# Infer format from file extension if not provided
|
|
30
|
-
field_type ||=
|
|
30
|
+
field_type ||= Opdotenv::FormatInferrer.infer_from_name(file) || :dotenv
|
|
31
31
|
text = File.read(file)
|
|
32
32
|
data = Opdotenv::Loader.parse_by_format(text, field_type)
|
|
33
33
|
Opdotenv::Exporter.export(path: path, data: data, field_type: field_type)
|
|
@@ -35,11 +35,3 @@ else
|
|
|
35
35
|
warn "Usage: opdotenv (read|export) ..."
|
|
36
36
|
exit 1
|
|
37
37
|
end
|
|
38
|
-
|
|
39
|
-
def infer_format_from_file(file)
|
|
40
|
-
case File.extname(file)
|
|
41
|
-
when ".json" then :json
|
|
42
|
-
when ".yaml", ".yml" then :yaml
|
|
43
|
-
else :dotenv
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -79,26 +79,38 @@ module Opdotenv
|
|
|
79
79
|
def log_available_fields(original_data, env_prefix, matched_data)
|
|
80
80
|
return unless ENV["OPDOTENV_DEBUG"] == "true"
|
|
81
81
|
return unless defined?(Rails) && Rails.logger
|
|
82
|
+
return if original_data.keys.empty?
|
|
82
83
|
|
|
83
84
|
prefix_upper = env_prefix.upcase
|
|
84
85
|
prefix_with_underscore = "#{prefix_upper}_"
|
|
85
|
-
|
|
86
86
|
available_fields = original_data.keys.map(&:to_s)
|
|
87
87
|
matched_fields = matched_data.keys.map(&:to_s)
|
|
88
|
+
unmatched = unmatched_fields(available_fields, prefix_upper, prefix_with_underscore)
|
|
89
|
+
|
|
90
|
+
log_field_summary(available_fields, matched_fields, env_prefix, prefix_with_underscore)
|
|
91
|
+
log_unmatched_fields(unmatched, prefix_with_underscore) if unmatched.any?
|
|
92
|
+
end
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
def unmatched_fields(available_fields, prefix_upper, prefix_with_underscore)
|
|
95
|
+
available_fields.reject do |field|
|
|
91
96
|
field_upper = field.to_s.upcase
|
|
92
97
|
field_upper.start_with?(prefix_with_underscore) || field_upper == prefix_upper
|
|
93
98
|
end
|
|
99
|
+
end
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
def log_field_summary(available_fields, matched_fields, env_prefix, prefix_with_underscore)
|
|
102
|
+
Rails.logger.debug { "[opdotenv] Available fields from 1Password: #{available_fields.join(", ")}" }
|
|
103
|
+
Rails.logger.debug do
|
|
104
|
+
"[opdotenv] Matched fields for #{env_prefix} (prefixed with #{prefix_with_underscore}, case-insensitive): #{matched_fields.join(", ")}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def log_unmatched_fields(unmatched, prefix_with_underscore)
|
|
109
|
+
Rails.logger.debug do
|
|
110
|
+
"[opdotenv] Unmatched fields (must be prefixed with #{prefix_with_underscore}, case-insensitive): #{unmatched.join(", ")}"
|
|
111
|
+
end
|
|
112
|
+
Rails.logger.debug do
|
|
113
|
+
"[opdotenv] To use these fields, rename them in 1Password to include the #{prefix_with_underscore} prefix"
|
|
102
114
|
end
|
|
103
115
|
end
|
|
104
116
|
end
|
|
@@ -18,27 +18,19 @@ module Opdotenv
|
|
|
18
18
|
@env = env
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Parse op:// style path or connect:// style path
|
|
22
|
-
# op://Vault/Item -> {vault: "Vault", item: "Item", field: nil}
|
|
23
|
-
# op://Vault/Item/notesPlain -> {vault: "Vault", item: "Item", field: "notesPlain"}
|
|
24
|
-
# op://Vault/Item/Section/Field -> {vault: "Vault", item: "Item", field: "Field"}
|
|
25
21
|
def read(path)
|
|
26
22
|
parsed = parse_path(path)
|
|
27
23
|
item = get_item(parsed[:vault], parsed[:item])
|
|
28
|
-
|
|
29
24
|
if parsed[:field]
|
|
30
|
-
# Read specific field
|
|
31
25
|
field = find_field(item, parsed[:field])
|
|
32
26
|
field ? field["value"] : ""
|
|
33
27
|
else
|
|
34
|
-
# Read notesPlain for secure notes
|
|
35
28
|
notes_field = item["fields"]&.find { |f| f["purpose"] == NOTES_PURPOSE || f["label"] == NOTES_PLAIN_FIELD }
|
|
36
29
|
notes_field ? notes_field["value"] : ""
|
|
37
30
|
end
|
|
38
31
|
end
|
|
39
32
|
|
|
40
33
|
def item_get(item_title, vault: nil)
|
|
41
|
-
# If vault not provided, search all vaults (requires listing vaults)
|
|
42
34
|
if vault.nil?
|
|
43
35
|
item = find_item_in_all_vaults(item_title)
|
|
44
36
|
raise ConnectApiError, "Item '#{item_title}' not found" unless item
|
|
@@ -62,86 +54,61 @@ module Opdotenv
|
|
|
62
54
|
|
|
63
55
|
def item_create_note(vault:, title:, notes:)
|
|
64
56
|
vault_id = vault_name_to_id(vault)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
response = api_request(:post, "/v1/vaults/#{vault_id}/items", payload)
|
|
57
|
+
existing = item_by_title_in_vault(vault_id, title)
|
|
58
|
+
response = if existing
|
|
59
|
+
api_request(:patch, "/v1/vaults/#{vault_id}/items/#{existing["id"]}", notes_patch_ops(existing, notes))
|
|
60
|
+
else
|
|
61
|
+
api_request(:post, "/v1/vaults/#{vault_id}/items", {
|
|
62
|
+
"vault" => {"id" => vault_id},
|
|
63
|
+
"title" => title,
|
|
64
|
+
"category" => SECURE_NOTE_CATEGORY,
|
|
65
|
+
"fields" => [{"purpose" => NOTES_PURPOSE, "value" => notes}]
|
|
66
|
+
})
|
|
67
|
+
end
|
|
79
68
|
JSON.pretty_generate(response)
|
|
80
69
|
end
|
|
81
70
|
|
|
82
71
|
def item_create_or_update_fields(vault:, item:, fields: {})
|
|
83
72
|
vault_id = vault_name_to_id(vault)
|
|
84
|
-
|
|
85
|
-
# Try to find existing item by title
|
|
86
73
|
existing = item_by_title_in_vault(vault_id, item)
|
|
87
|
-
|
|
88
74
|
if existing
|
|
89
|
-
|
|
90
|
-
fields_array = fields.map do |k, v|
|
|
91
|
-
existing_field = existing["fields"]&.find { |f| f["label"] == k.to_s }
|
|
92
|
-
if existing_field
|
|
93
|
-
{
|
|
94
|
-
"op" => "replace",
|
|
95
|
-
"path" => "/fields/#{existing_field["id"]}/value",
|
|
96
|
-
"value" => v.to_s
|
|
97
|
-
}
|
|
98
|
-
else
|
|
99
|
-
{
|
|
100
|
-
"op" => "add",
|
|
101
|
-
"path" => "/fields",
|
|
102
|
-
"value" => {
|
|
103
|
-
"type" => "CONCEALED",
|
|
104
|
-
"label" => k.to_s,
|
|
105
|
-
"value" => v.to_s
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
api_request(:patch, "/v1/vaults/#{vault_id}/items/#{existing["id"]}", fields_array)
|
|
75
|
+
api_request(:patch, "/v1/vaults/#{vault_id}/items/#{existing["id"]}", build_patch_fields(existing, fields))
|
|
112
76
|
else
|
|
113
|
-
|
|
114
|
-
fields_array = fields.map do |k, v|
|
|
115
|
-
{
|
|
116
|
-
"type" => "CONCEALED",
|
|
117
|
-
"label" => k.to_s,
|
|
118
|
-
"value" => v.to_s
|
|
119
|
-
}
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
payload = {
|
|
77
|
+
api_request(:post, "/v1/vaults/#{vault_id}/items", {
|
|
123
78
|
"vault" => {"id" => vault_id},
|
|
124
79
|
"title" => item,
|
|
125
80
|
"category" => LOGIN_CATEGORY,
|
|
126
|
-
"fields" =>
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
api_request(:post, "/v1/vaults/#{vault_id}/items", payload)
|
|
81
|
+
"fields" => fields.map { |k, v| {"type" => "CONCEALED", "label" => k.to_s, "value" => v.to_s} }
|
|
82
|
+
})
|
|
130
83
|
end
|
|
131
84
|
end
|
|
132
85
|
|
|
133
86
|
private
|
|
134
87
|
|
|
88
|
+
def notes_patch_ops(existing, notes)
|
|
89
|
+
field = existing["fields"]&.find { |entry| entry["purpose"] == NOTES_PURPOSE || entry["label"] == NOTES_PLAIN_FIELD }
|
|
90
|
+
if field
|
|
91
|
+
[{"op" => "replace", "path" => "/fields/#{field["id"]}/value", "value" => notes}]
|
|
92
|
+
else
|
|
93
|
+
[{"op" => "add", "path" => "/fields", "value" => {"purpose" => NOTES_PURPOSE, "value" => notes}}]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_patch_fields(existing, fields)
|
|
98
|
+
fields.map do |key, value|
|
|
99
|
+
existing_field = existing["fields"]&.find { |field| field["label"] == key.to_s }
|
|
100
|
+
if existing_field
|
|
101
|
+
{"op" => "replace", "path" => "/fields/#{existing_field["id"]}/value", "value" => value.to_s}
|
|
102
|
+
else
|
|
103
|
+
{"op" => "add", "path" => "/fields", "value" => {"type" => "CONCEALED", "label" => key.to_s, "value" => value.to_s}}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
135
108
|
def parse_path(path)
|
|
136
|
-
# op://Vault/Item or connect://Vault/Item
|
|
137
109
|
match = path.match(/\A(?:op|connect):\/\/([^\/]+)\/([^\/]+)(?:\/(.+))?\z/)
|
|
138
110
|
raise ConnectApiError, "Invalid path format: #{path}" unless match
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
vault: match[1],
|
|
142
|
-
item: match[2],
|
|
143
|
-
field: match[3]
|
|
144
|
-
}
|
|
111
|
+
{vault: match[1], item: match[2], field: match[3]}
|
|
145
112
|
end
|
|
146
113
|
|
|
147
114
|
def vault_name_to_id(vault_name)
|
|
@@ -164,20 +131,23 @@ module Opdotenv
|
|
|
164
131
|
item = item_by_title_in_vault(vault_id, item_title)
|
|
165
132
|
raise ConnectApiError, "Item '#{item_title}' not found in vault '#{vault_name}'" unless item
|
|
166
133
|
|
|
167
|
-
|
|
168
|
-
api_request(:get, "/v1/vaults/#{vault_id}/items/#{item["id"]}")
|
|
134
|
+
item
|
|
169
135
|
end
|
|
170
136
|
|
|
171
137
|
def item_by_title_in_vault(vault_id, item_title)
|
|
172
|
-
|
|
173
|
-
items = api_request(:get, "/v1/vaults/#{vault_id}/items")
|
|
174
|
-
item = items.find { |
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
138
|
+
filter = URI.encode_www_form_component(%(title eq "#{item_title.to_s.gsub('"', '\\"')}"))
|
|
139
|
+
items = api_request(:get, "/v1/vaults/#{vault_id}/items?filter=#{filter}")
|
|
140
|
+
item = Array(items).find { |entry| entry["title"] == item_title || entry["id"] == item_title }
|
|
141
|
+
return item_by_uuid(vault_id, item_title) if item.nil?
|
|
142
|
+
item["id"] ? api_request(:get, "/v1/vaults/#{vault_id}/items/#{item["id"]}") : item
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def item_by_uuid(vault_id, item_id)
|
|
146
|
+
return nil unless item_id.to_s.match?(/\A[0-9a-f-]{36}\z/i)
|
|
147
|
+
|
|
148
|
+
api_request(:get, "/v1/vaults/#{vault_id}/items/#{item_id}")
|
|
149
|
+
rescue ConnectApiError
|
|
150
|
+
nil
|
|
181
151
|
end
|
|
182
152
|
|
|
183
153
|
def find_field(item, field_name)
|
|
@@ -199,12 +169,8 @@ module Opdotenv
|
|
|
199
169
|
end
|
|
200
170
|
|
|
201
171
|
def build_uri(path)
|
|
202
|
-
# Validate path to prevent path traversal attacks
|
|
203
|
-
# API paths legitimately start with "/", so only check for ".."
|
|
204
172
|
raise ConnectApiError, "Invalid path: #{path}" if path.include?("..")
|
|
205
173
|
|
|
206
|
-
# Use URI.join which handles path normalization safely
|
|
207
|
-
# URI.join expects base URL without trailing slash for proper joining
|
|
208
174
|
base_uri = @base_url.end_with?("/") ? @base_url[0..-2] : @base_url
|
|
209
175
|
normalized_path = path.start_with?("/") ? path : "/#{path}"
|
|
210
176
|
URI.join(base_uri, normalized_path)
|
|
@@ -241,7 +207,7 @@ module Opdotenv
|
|
|
241
207
|
attempts += 1
|
|
242
208
|
http.request(request)
|
|
243
209
|
rescue Timeout::Error, Errno::ECONNRESET
|
|
244
|
-
raise if attempts >= 2
|
|
210
|
+
raise if attempts >= 2 || !request.is_a?(Net::HTTP::Get)
|
|
245
211
|
sleep 0.2
|
|
246
212
|
retry
|
|
247
213
|
end
|
|
@@ -251,9 +217,13 @@ module Opdotenv
|
|
|
251
217
|
code = response.code.to_i
|
|
252
218
|
|
|
253
219
|
case code
|
|
254
|
-
when 200
|
|
255
|
-
return {} if response.body.empty? || code == 204
|
|
256
|
-
|
|
220
|
+
when 200..299
|
|
221
|
+
return {} if response.body.to_s.empty? || code == 204
|
|
222
|
+
begin
|
|
223
|
+
JSON.parse(response.body)
|
|
224
|
+
rescue JSON::ParserError
|
|
225
|
+
raise ConnectApiError, "API error (#{code}): Invalid JSON response"
|
|
226
|
+
end
|
|
257
227
|
when 401
|
|
258
228
|
raise ConnectApiError, "Unauthorized: Invalid or missing access token"
|
|
259
229
|
when 403
|
|
@@ -263,24 +233,10 @@ module Opdotenv
|
|
|
263
233
|
when 500..599
|
|
264
234
|
raise ConnectApiError, "API error (#{code}): Server error"
|
|
265
235
|
else
|
|
266
|
-
|
|
267
|
-
safe_message = extract_safe_error_message(response)
|
|
268
|
-
raise ConnectApiError, "API error (#{code}): #{safe_message}"
|
|
236
|
+
raise ConnectApiError, "API error (#{code}): Request failed"
|
|
269
237
|
end
|
|
270
238
|
end
|
|
271
239
|
|
|
272
|
-
def extract_safe_error_message(response)
|
|
273
|
-
# Only extract structured error messages from JSON responses
|
|
274
|
-
# Never include raw response body to avoid leaking secrets
|
|
275
|
-
|
|
276
|
-
parsed = JSON.parse(response.body)
|
|
277
|
-
# Only return known safe fields that are typically error messages
|
|
278
|
-
parsed["message"] || parsed["error"] || "Request failed"
|
|
279
|
-
rescue JSON::ParserError
|
|
280
|
-
# For non-JSON responses, return generic message to avoid leaking body
|
|
281
|
-
"Request failed"
|
|
282
|
-
end
|
|
283
|
-
|
|
284
240
|
def validate_url(url)
|
|
285
241
|
uri = URI.parse(url)
|
|
286
242
|
unless ["http", "https"].include?(uri.scheme)
|
data/lib/opdotenv/exporter.rb
CHANGED
|
@@ -18,7 +18,6 @@ module Opdotenv
|
|
|
18
18
|
|
|
19
19
|
# Check if path matches format patterns (Secure Note) or regular item (fields)
|
|
20
20
|
if FormatInferrer.matches_format_pattern?(item_name)
|
|
21
|
-
# Create Secure Note
|
|
22
21
|
field_type ||= FormatInferrer.infer_from_name(item_name) || :dotenv
|
|
23
22
|
content = serialize_by_format(data, field_type)
|
|
24
23
|
client.item_create_note(vault: vault, title: item_name, notes: content)
|
|
@@ -3,18 +3,25 @@ module Opdotenv
|
|
|
3
3
|
module FormatInferrer
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
|
-
DOTENV_PATTERN = /\.env\.?/
|
|
7
6
|
JSON_EXTENSIONS = [".json"].freeze
|
|
8
7
|
YAML_EXTENSIONS = [".yaml", ".yml"].freeze
|
|
9
8
|
|
|
10
9
|
def infer_from_name(name)
|
|
11
|
-
|
|
12
|
-
return :
|
|
13
|
-
return :
|
|
10
|
+
base = File.basename(name.to_s)
|
|
11
|
+
return :dotenv if dotenv_basename?(base)
|
|
12
|
+
return :json if JSON_EXTENSIONS.any? { |ext| base.end_with?(ext) }
|
|
13
|
+
return :yaml if YAML_EXTENSIONS.any? { |ext| base.end_with?(ext) }
|
|
14
14
|
|
|
15
15
|
nil
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def dotenv_basename?(base)
|
|
19
|
+
base == ".env" ||
|
|
20
|
+
base.start_with?(".env.") ||
|
|
21
|
+
base.include?(" .env.") ||
|
|
22
|
+
base.end_with?(" .env")
|
|
23
|
+
end
|
|
24
|
+
|
|
18
25
|
def matches_format_pattern?(name)
|
|
19
26
|
!infer_from_name(name).nil?
|
|
20
27
|
end
|
data/lib/opdotenv/loader.rb
CHANGED
|
@@ -25,24 +25,25 @@ module Opdotenv
|
|
|
25
25
|
raw_json = client.item_get(item, vault: vault)
|
|
26
26
|
item_hash = parse_json_safe(raw_json)
|
|
27
27
|
|
|
28
|
-
# Only process fields from the 1Password item, filter out empty values
|
|
29
28
|
item_hash["fields"]&.each_with_object({}) do |field, env_data|
|
|
30
|
-
|
|
31
|
-
next unless
|
|
29
|
+
entry = field_env_entry(field)
|
|
30
|
+
next unless entry
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
env_data[entry[:key]] = entry[:value]
|
|
33
|
+
end || {}
|
|
34
|
+
end
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
def self.field_env_entry(field)
|
|
37
|
+
return nil unless field.is_a?(Hash)
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
label = (field["label"] || field["id"])&.strip
|
|
40
|
+
return nil if label.nil? || label.empty?
|
|
41
|
+
return nil if field["purpose"] == NOTES_PURPOSE
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
value = field["value"]
|
|
44
|
+
return nil if value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
45
|
+
|
|
46
|
+
{key: label.to_s, value: value.to_s}
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def self.build_field_path(path, field_name)
|
data/lib/opdotenv/op_client.rb
CHANGED
|
@@ -27,33 +27,48 @@ module Opdotenv
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def item_create_note(vault:, title:, notes:)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
notes_field = {"id" => NOTES_PLAIN_FIELD, "type" => "STRING", "purpose" => "NOTES", "value" => notes}
|
|
31
|
+
if item_exists?(title, vault: vault)
|
|
32
|
+
payload = JSON.parse(item_get(title, vault: vault))
|
|
33
|
+
existing = payload["fields"]&.find { |field|
|
|
34
|
+
field["id"] == NOTES_PLAIN_FIELD || field["purpose"] == "NOTES" || field["label"] == NOTES_PLAIN_FIELD
|
|
35
|
+
}
|
|
36
|
+
if existing
|
|
37
|
+
existing["value"] = notes
|
|
38
|
+
else
|
|
39
|
+
payload["fields"] ||= []
|
|
40
|
+
payload["fields"] << notes_field
|
|
41
|
+
end
|
|
42
|
+
capture([@cli_path, "item", "edit", title, "--vault", vault], stdin_data: JSON.generate(payload))
|
|
43
|
+
else
|
|
44
|
+
args = [
|
|
45
|
+
@cli_path, "item", "create",
|
|
46
|
+
"--category", SECURE_NOTE_CATEGORY,
|
|
47
|
+
"--title", title,
|
|
48
|
+
"--vault", vault,
|
|
49
|
+
"-"
|
|
50
|
+
]
|
|
51
|
+
capture(args, stdin_data: JSON.generate({"fields" => [notes_field]}))
|
|
52
|
+
end
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def item_create_or_update_fields(vault:, item:, fields: {})
|
|
43
56
|
exists = item_exists?(item, vault: vault)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
payload = exists ? JSON.parse(item_get(item, vault: vault)) : {"category" => LOGIN_CATEGORY, "fields" => []}
|
|
58
|
+
fields.each do |key, value|
|
|
59
|
+
field = payload["fields"].find { |entry| entry["label"] == key.to_s || entry["id"] == key.to_s }
|
|
60
|
+
if field
|
|
61
|
+
field["value"] = value.to_s
|
|
62
|
+
else
|
|
63
|
+
payload["fields"] << {"type" => "CONCEALED", "label" => key.to_s, "value" => value.to_s}
|
|
49
64
|
end
|
|
65
|
+
end
|
|
66
|
+
args = if exists
|
|
67
|
+
[@cli_path, "item", "edit", item, "--vault", vault]
|
|
50
68
|
else
|
|
51
|
-
|
|
52
|
-
fields.each do |k, v|
|
|
53
|
-
args += ["--set", "#{k}=#{v}"]
|
|
54
|
-
end
|
|
55
|
-
capture(args)
|
|
69
|
+
[@cli_path, "item", "create", "--title", item, "--vault", vault, "-"]
|
|
56
70
|
end
|
|
71
|
+
capture(args, stdin_data: JSON.generate(payload))
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
private
|
|
@@ -70,10 +85,10 @@ module Opdotenv
|
|
|
70
85
|
raise ArgumentError, "Invalid path format: #{path.inspect}. Must start with 'op://'"
|
|
71
86
|
end
|
|
72
87
|
|
|
73
|
-
def capture(args)
|
|
88
|
+
def capture(args, **options)
|
|
74
89
|
# Use exec-style array to prevent shell injection
|
|
75
90
|
# Open3.capture2e captures both stdout and stderr, and properly returns exit status
|
|
76
|
-
out, status = Open3.capture2e(*args)
|
|
91
|
+
out, status = Open3.capture2e(*args, **options)
|
|
77
92
|
|
|
78
93
|
# For JSON output, try to parse even if exit code is non-zero
|
|
79
94
|
# Some op commands may return non-zero but still output valid JSON
|
|
@@ -2,26 +2,33 @@ module Opdotenv
|
|
|
2
2
|
module Parsers
|
|
3
3
|
class DotenvParser
|
|
4
4
|
def self.parse(text)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
text.to_s.each_line.with_object({}) do |line, env|
|
|
6
|
+
entry = parse_line(line)
|
|
7
|
+
next unless entry
|
|
8
|
+
|
|
9
|
+
env[entry[:key]] = entry[:value]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.parse_line(line)
|
|
14
|
+
line = line.strip
|
|
15
|
+
return nil if line.empty? || line.start_with?("#")
|
|
16
|
+
|
|
17
|
+
line = line.sub(/^export\s+/, "")
|
|
18
|
+
match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\z/)
|
|
19
|
+
return nil unless match
|
|
20
|
+
|
|
21
|
+
{key: match[1], value: unquote_value(match[2])}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.unquote_value(raw)
|
|
25
|
+
if raw.start_with?("\"") && raw.end_with?("\"")
|
|
26
|
+
raw[1..-2].gsub('\\"', '"')
|
|
27
|
+
elsif raw.start_with?("'") && raw.end_with?("'")
|
|
28
|
+
raw[1..-2]
|
|
29
|
+
else
|
|
30
|
+
raw
|
|
23
31
|
end
|
|
24
|
-
env
|
|
25
32
|
end
|
|
26
33
|
end
|
|
27
34
|
end
|
|
@@ -19,18 +19,17 @@ module Opdotenv
|
|
|
19
19
|
|
|
20
20
|
path = source.to_s
|
|
21
21
|
_, item = parse_op_path(path)
|
|
22
|
-
|
|
23
|
-
# Extract item name and potential field name
|
|
24
|
-
# Handle paths like "op://Vault/Item" or "op://Vault/Item Name/field"
|
|
25
22
|
item_parts = item.split("/")
|
|
26
23
|
item_name = item_parts.first
|
|
27
24
|
field_name = (item_parts.length > 1) ? item_parts[1] : nil
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
build_parsed_source(path, item_name, field_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_parsed_source(path, item_name, field_name)
|
|
30
30
|
if field_name
|
|
31
31
|
field_type = FormatInferrer.infer_from_name(field_name)
|
|
32
32
|
{path: path, field_name: field_name, field_type: field_type}
|
|
33
|
-
# If no field name in path, infer from item name patterns
|
|
34
33
|
elsif (field_type = FormatInferrer.infer_from_name(item_name))
|
|
35
34
|
{path: path, field_name: NOTES_PLAIN_FIELD, field_type: field_type}
|
|
36
35
|
else
|
|
@@ -66,6 +65,6 @@ module Opdotenv
|
|
|
66
65
|
}
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
module_function :normalize_hash
|
|
68
|
+
module_function :normalize_hash, :build_parsed_source
|
|
70
69
|
end
|
|
71
70
|
end
|
data/lib/opdotenv/version.rb
CHANGED
data/lib/opdotenv.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opdotenv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -15,17 +15,17 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '7.0'
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '9.0'
|
|
22
|
-
type: :
|
|
22
|
+
type: :development
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '
|
|
28
|
+
version: '7.0'
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9.0'
|
|
@@ -58,75 +58,131 @@ dependencies:
|
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '3'
|
|
60
60
|
- !ruby/object:Gem::Dependency
|
|
61
|
-
name:
|
|
61
|
+
name: polyrun
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 2.2.0
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 2.2.0
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: standard
|
|
62
76
|
requirement: !ruby/object:Gem::Requirement
|
|
63
77
|
requirements:
|
|
64
78
|
- - "~>"
|
|
65
79
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
80
|
+
version: '1.52'
|
|
67
81
|
type: :development
|
|
68
82
|
prerelease: false
|
|
69
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
84
|
requirements:
|
|
71
85
|
- - "~>"
|
|
72
86
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
87
|
+
version: '1.52'
|
|
74
88
|
- !ruby/object:Gem::Dependency
|
|
75
|
-
name:
|
|
89
|
+
name: standard-custom
|
|
76
90
|
requirement: !ruby/object:Gem::Requirement
|
|
77
91
|
requirements:
|
|
78
92
|
- - "~>"
|
|
79
93
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0
|
|
94
|
+
version: '1.0'
|
|
81
95
|
type: :development
|
|
82
96
|
prerelease: false
|
|
83
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
98
|
requirements:
|
|
85
99
|
- - "~>"
|
|
86
100
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '0
|
|
101
|
+
version: '1.0'
|
|
88
102
|
- !ruby/object:Gem::Dependency
|
|
89
|
-
name:
|
|
103
|
+
name: standard-performance
|
|
90
104
|
requirement: !ruby/object:Gem::Requirement
|
|
91
105
|
requirements:
|
|
92
106
|
- - "~>"
|
|
93
107
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '
|
|
108
|
+
version: '1.8'
|
|
95
109
|
type: :development
|
|
96
110
|
prerelease: false
|
|
97
111
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
112
|
requirements:
|
|
99
113
|
- - "~>"
|
|
100
114
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: '
|
|
115
|
+
version: '1.8'
|
|
102
116
|
- !ruby/object:Gem::Dependency
|
|
103
|
-
name: standard
|
|
117
|
+
name: standard-rails
|
|
104
118
|
requirement: !ruby/object:Gem::Requirement
|
|
105
119
|
requirements:
|
|
106
120
|
- - "~>"
|
|
107
121
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '1'
|
|
122
|
+
version: '1.5'
|
|
109
123
|
type: :development
|
|
110
124
|
prerelease: false
|
|
111
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
112
126
|
requirements:
|
|
113
127
|
- - "~>"
|
|
114
128
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: '1'
|
|
129
|
+
version: '1.5'
|
|
116
130
|
- !ruby/object:Gem::Dependency
|
|
117
|
-
name: standard-
|
|
131
|
+
name: standard-rspec
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0.3'
|
|
137
|
+
type: :development
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0.3'
|
|
144
|
+
- !ruby/object:Gem::Dependency
|
|
145
|
+
name: rubocop-rails
|
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - "~>"
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '2.33'
|
|
151
|
+
type: :development
|
|
152
|
+
prerelease: false
|
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - "~>"
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '2.33'
|
|
158
|
+
- !ruby/object:Gem::Dependency
|
|
159
|
+
name: rubocop-rspec
|
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - "~>"
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: '3.8'
|
|
165
|
+
type: :development
|
|
166
|
+
prerelease: false
|
|
167
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - "~>"
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: '3.8'
|
|
172
|
+
- !ruby/object:Gem::Dependency
|
|
173
|
+
name: rubocop-thread_safety
|
|
118
174
|
requirement: !ruby/object:Gem::Requirement
|
|
119
175
|
requirements:
|
|
120
176
|
- - "~>"
|
|
121
177
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: '
|
|
178
|
+
version: '0.7'
|
|
123
179
|
type: :development
|
|
124
180
|
prerelease: false
|
|
125
181
|
version_requirements: !ruby/object:Gem::Requirement
|
|
126
182
|
requirements:
|
|
127
183
|
- - "~>"
|
|
128
184
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: '
|
|
185
|
+
version: '0.7'
|
|
130
186
|
- !ruby/object:Gem::Dependency
|
|
131
187
|
name: appraisal
|
|
132
188
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -239,14 +295,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
239
295
|
requirements:
|
|
240
296
|
- - ">="
|
|
241
297
|
- !ruby/object:Gem::Version
|
|
242
|
-
version: '
|
|
298
|
+
version: '3.4'
|
|
243
299
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
244
300
|
requirements:
|
|
245
301
|
- - ">="
|
|
246
302
|
- !ruby/object:Gem::Version
|
|
247
303
|
version: '0'
|
|
248
304
|
requirements: []
|
|
249
|
-
rubygems_version:
|
|
305
|
+
rubygems_version: 4.0.6
|
|
250
306
|
specification_version: 4
|
|
251
307
|
summary: Load and export environment variables with 1Password CLI or Connect API
|
|
252
308
|
test_files: []
|