envref 0.1.0__tar.gz
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.
- envref-0.1.0/.gitignore +10 -0
- envref-0.1.0/CHANGELOG.md +13 -0
- envref-0.1.0/LICENSE +21 -0
- envref-0.1.0/PKG-INFO +188 -0
- envref-0.1.0/README.md +158 -0
- envref-0.1.0/SECURITY.md +52 -0
- envref-0.1.0/pyproject.toml +66 -0
- envref-0.1.0/src/envref/__init__.py +40 -0
- envref-0.1.0/src/envref/cli.py +166 -0
- envref-0.1.0/src/envref/core.py +135 -0
- envref-0.1.0/src/envref/parse.py +76 -0
- envref-0.1.0/src/envref/resolvers/__init__.py +33 -0
- envref-0.1.0/src/envref/resolvers/env.py +29 -0
- envref-0.1.0/src/envref/resolvers/file.py +33 -0
- envref-0.1.0/src/envref/resolvers/keychain.py +80 -0
- envref-0.1.0/src/envref/resolvers/op.py +347 -0
- envref-0.1.0/src/envref/touchid/helper.swift +76 -0
- envref-0.1.0/src/envref/touchid.py +66 -0
- envref-0.1.0/tests/conftest.py +89 -0
- envref-0.1.0/tests/test_cli.py +74 -0
- envref-0.1.0/tests/test_core.py +65 -0
- envref-0.1.0/tests/test_op.py +101 -0
- envref-0.1.0/tests/test_parse.py +33 -0
- envref-0.1.0/tests/test_resolvers_simple.py +41 -0
envref-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-11-07
|
|
4
|
+
|
|
5
|
+
First release. Extracted from a private automation repository where the
|
|
6
|
+
1Password path had run daily for a year.
|
|
7
|
+
|
|
8
|
+
- `.env` files whose values may be `{{ scheme://… }}` references.
|
|
9
|
+
- Resolvers: `op://` (1Password, via the `op` CLI with a cached session and
|
|
10
|
+
Touch ID sign-in, or via a service account), `keychain://` (macOS Keychain),
|
|
11
|
+
`file://`, `env://`.
|
|
12
|
+
- Third-party resolvers through the `envref.resolvers` entry-point group.
|
|
13
|
+
- CLI: `envref run`, `export`, `check`, `doctor`, `op save-password`.
|
envref-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Haglund
|
|
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.
|
envref-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: envref
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A .env where any value can be a reference into a secret store, resolved at load time. op://, keychain://, file://, env://, and your own.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sebaheg/envref
|
|
6
|
+
Project-URL: Repository, https://github.com/sebaheg/envref
|
|
7
|
+
Project-URL: Issues, https://github.com/sebaheg/envref/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/sebaheg/envref/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Sebastian Haglund
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: 1password,dotenv,environment,keychain,secrets,twelve-factor
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Provides-Extra: op
|
|
28
|
+
Requires-Dist: onepassword-sdk>=0.3; (python_version < '3.14') and extra == 'op'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# envref
|
|
32
|
+
|
|
33
|
+
A `.env` where any value can be a reference into a secret store, resolved
|
|
34
|
+
when the file is loaded.
|
|
35
|
+
|
|
36
|
+
```dotenv
|
|
37
|
+
# .env
|
|
38
|
+
ATTIO_KEY={{ op://work/Attio/api_key }} # 1Password
|
|
39
|
+
DB_PASSWORD={{ keychain://myapp/postgres }} # macOS Keychain
|
|
40
|
+
TLS_KEY={{ file://~/.ssh/server.pem }} # a file's contents
|
|
41
|
+
GITHUB_TOKEN={{ env://GH_TOKEN }} # another variable
|
|
42
|
+
REGION=eu-north-1 # a plain literal
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
envref run -- python app.py # app.py sees ATTIO_KEY=<the real key>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import envref
|
|
51
|
+
envref.load(".env") # same thing, into os.environ
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The file holds no secrets, so it can be committed. The scheme of each
|
|
55
|
+
reference picks the backend. Four ship built in, and your own takes one class
|
|
56
|
+
and one entry point.
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
`op run`, `berglas exec`, `chamber exec` and friends each do this for one
|
|
61
|
+
store. Teams rarely have one store. A laptop has 1Password and Keychain, a
|
|
62
|
+
container has a service account, a CI job has its own vault, and the same
|
|
63
|
+
`.env` should work in all three with the same names. `envref` keeps the file
|
|
64
|
+
format (plain dotenv, plus `{{ scheme://… }}`), and makes the backend a plugin.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv tool install envref # CLI
|
|
70
|
+
pip install envref # library, zero dependencies
|
|
71
|
+
pip install 'envref[op]' # + 1Password service-account support (SDK)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The `op://` CLI path needs only the [`op`](https://developer.1password.com/docs/cli/)
|
|
75
|
+
binary on PATH, no extra.
|
|
76
|
+
|
|
77
|
+
## Resolvers
|
|
78
|
+
|
|
79
|
+
| Scheme | Resolves to | Needs |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `op://vault/item/field` | a 1Password field | `op` CLI, or `OP_SERVICE_ACCOUNT_TOKEN` + `envref[op]` |
|
|
82
|
+
| `keychain://service/account` | a macOS Keychain generic password | macOS |
|
|
83
|
+
| `file://path` | the file's contents, one trailing newline dropped | — |
|
|
84
|
+
| `env://NAME` | another environment variable | — |
|
|
85
|
+
|
|
86
|
+
A value written as `{{ scheme://… }}` is always a reference, and an unknown
|
|
87
|
+
scheme is an error. A bare `scheme://…` without braces is resolved only when a
|
|
88
|
+
resolver for that scheme is installed, so `URL=https://example.com` stays a
|
|
89
|
+
literal and `KEY=op://vault/item/field` still works for files written for
|
|
90
|
+
`op read`.
|
|
91
|
+
|
|
92
|
+
### 1Password in detail
|
|
93
|
+
|
|
94
|
+
Two modes, chosen by the environment:
|
|
95
|
+
|
|
96
|
+
- **Service account** when `OP_SERVICE_ACCOUNT_TOKEN` is set. The SDK resolves
|
|
97
|
+
every reference in one round trip. No binary, no biometrics. For containers.
|
|
98
|
+
- **CLI** otherwise. Every reference goes through one `op inject`. If `op` has
|
|
99
|
+
no session, `envref` signs in: the account password comes from Keychain
|
|
100
|
+
behind Touch ID if you stored it with `envref op save-password`, or from the
|
|
101
|
+
terminal if not. The session token is cached in `~/.cache/envref/` so a
|
|
102
|
+
burst of commands costs one sign-in; concurrent processes share it through
|
|
103
|
+
a lock. See [SECURITY.md](SECURITY.md) for what that cache means.
|
|
104
|
+
|
|
105
|
+
Session caching presumes "biometric unlock for the CLI" is off in the
|
|
106
|
+
1Password app. With it on, `op` prompts per invocation and `envref` holds no
|
|
107
|
+
token.
|
|
108
|
+
|
|
109
|
+
Knobs: `OP_ACCOUNT` (shorthand, when several accounts are signed in),
|
|
110
|
+
`ENVREF_OP_SESSION_TTL` (seconds, default 1500, `0` disables the cache),
|
|
111
|
+
`ENVREF_OP_KEYCHAIN_SERVICE` / `ENVREF_OP_KEYCHAIN_ACCOUNT` (where the
|
|
112
|
+
password is stored, default `envref-op` / the account's email).
|
|
113
|
+
|
|
114
|
+
## CLI
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
envref run [-f FILE]... [--override] -- cmd args # exec cmd with the resolved environment
|
|
118
|
+
envref export [-f FILE]... [--format shell|dotenv|json]
|
|
119
|
+
envref check [-f FILE]... # resolve and report per key; never prints a value
|
|
120
|
+
envref doctor # installed resolvers and whether each can run here
|
|
121
|
+
envref op save-password [--account SHORTHAND] # store the 1Password password in Keychain for Touch ID sign-in
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`-f` repeats. The first file to set a name wins, so `-f .env.refs -f .env`
|
|
125
|
+
lets a local `.env` fill in what the shared references file lacks without
|
|
126
|
+
overriding it. Default is `.env`.
|
|
127
|
+
|
|
128
|
+
## Library
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import envref
|
|
132
|
+
|
|
133
|
+
values = envref.load(".env.refs", ".env") # dict, also exported into os.environ
|
|
134
|
+
values = envref.load(".env", export=False) # dict only
|
|
135
|
+
envref.load(".env", override=True) # let the file win over existing variables
|
|
136
|
+
|
|
137
|
+
entries = envref.parse(text) # list[Entry], refs identified
|
|
138
|
+
envref.resolve(entries, registry={"op": ...}) # bring your own resolvers
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Errors are typed: `UnknownScheme`, `ResolverUnavailable` (the backend cannot
|
|
142
|
+
run here: no `op`, not macOS, no token) and `ResolveError` (which references
|
|
143
|
+
failed and why, in `.failures`). All derive from `EnvrefError`.
|
|
144
|
+
|
|
145
|
+
## Writing a resolver
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# myenvref/aws.py
|
|
149
|
+
class SecretsManagerResolver:
|
|
150
|
+
scheme = "awssm"
|
|
151
|
+
|
|
152
|
+
def resolve(self, refs: list[str]) -> dict[str, str]:
|
|
153
|
+
# refs are every awssm:// reference in the file, deduplicated: batch them.
|
|
154
|
+
...
|
|
155
|
+
return {ref: value for ref, value in ...}
|
|
156
|
+
|
|
157
|
+
def doctor(self) -> str: # optional, shown by `envref doctor`
|
|
158
|
+
return "ok"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```toml
|
|
162
|
+
# pyproject.toml
|
|
163
|
+
[project.entry-points."envref.resolvers"]
|
|
164
|
+
awssm = "myenvref.aws:SecretsManagerResolver"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Installed next to `envref`, `{{ awssm://… }}` now resolves. An entry point
|
|
168
|
+
with a built-in's name replaces it.
|
|
169
|
+
|
|
170
|
+
## Prior art
|
|
171
|
+
|
|
172
|
+
[`teller`](https://github.com/tellerops/teller) and
|
|
173
|
+
[`novops`](https://github.com/PierreBeucher/novops) are multi-provider too,
|
|
174
|
+
with the mapping in a YAML config rather than inline in the `.env`.
|
|
175
|
+
[`berglas`](https://github.com/GoogleCloudPlatform/berglas) has inline
|
|
176
|
+
`sm://` references, for Google Cloud only. [`dotenvx`](https://dotenvx.com)
|
|
177
|
+
encrypts the values rather than referencing them. `op inject` is what the
|
|
178
|
+
`{{ }}` syntax comes from, and any `.env.op` written for it is a valid
|
|
179
|
+
`envref` file.
|
|
180
|
+
|
|
181
|
+
## Status
|
|
182
|
+
|
|
183
|
+
Extracted in November 2026 from a private automation repository where the
|
|
184
|
+
1Password path had run daily for a year, on a laptop and in scheduled
|
|
185
|
+
containers. The other resolvers are new. macOS and Linux; Keychain and Touch
|
|
186
|
+
ID are macOS only.
|
|
187
|
+
|
|
188
|
+
MIT.
|
envref-0.1.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# envref
|
|
2
|
+
|
|
3
|
+
A `.env` where any value can be a reference into a secret store, resolved
|
|
4
|
+
when the file is loaded.
|
|
5
|
+
|
|
6
|
+
```dotenv
|
|
7
|
+
# .env
|
|
8
|
+
ATTIO_KEY={{ op://work/Attio/api_key }} # 1Password
|
|
9
|
+
DB_PASSWORD={{ keychain://myapp/postgres }} # macOS Keychain
|
|
10
|
+
TLS_KEY={{ file://~/.ssh/server.pem }} # a file's contents
|
|
11
|
+
GITHUB_TOKEN={{ env://GH_TOKEN }} # another variable
|
|
12
|
+
REGION=eu-north-1 # a plain literal
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
envref run -- python app.py # app.py sees ATTIO_KEY=<the real key>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import envref
|
|
21
|
+
envref.load(".env") # same thing, into os.environ
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The file holds no secrets, so it can be committed. The scheme of each
|
|
25
|
+
reference picks the backend. Four ship built in, and your own takes one class
|
|
26
|
+
and one entry point.
|
|
27
|
+
|
|
28
|
+
## Why
|
|
29
|
+
|
|
30
|
+
`op run`, `berglas exec`, `chamber exec` and friends each do this for one
|
|
31
|
+
store. Teams rarely have one store. A laptop has 1Password and Keychain, a
|
|
32
|
+
container has a service account, a CI job has its own vault, and the same
|
|
33
|
+
`.env` should work in all three with the same names. `envref` keeps the file
|
|
34
|
+
format (plain dotenv, plus `{{ scheme://… }}`), and makes the backend a plugin.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv tool install envref # CLI
|
|
40
|
+
pip install envref # library, zero dependencies
|
|
41
|
+
pip install 'envref[op]' # + 1Password service-account support (SDK)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `op://` CLI path needs only the [`op`](https://developer.1password.com/docs/cli/)
|
|
45
|
+
binary on PATH, no extra.
|
|
46
|
+
|
|
47
|
+
## Resolvers
|
|
48
|
+
|
|
49
|
+
| Scheme | Resolves to | Needs |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `op://vault/item/field` | a 1Password field | `op` CLI, or `OP_SERVICE_ACCOUNT_TOKEN` + `envref[op]` |
|
|
52
|
+
| `keychain://service/account` | a macOS Keychain generic password | macOS |
|
|
53
|
+
| `file://path` | the file's contents, one trailing newline dropped | — |
|
|
54
|
+
| `env://NAME` | another environment variable | — |
|
|
55
|
+
|
|
56
|
+
A value written as `{{ scheme://… }}` is always a reference, and an unknown
|
|
57
|
+
scheme is an error. A bare `scheme://…` without braces is resolved only when a
|
|
58
|
+
resolver for that scheme is installed, so `URL=https://example.com` stays a
|
|
59
|
+
literal and `KEY=op://vault/item/field` still works for files written for
|
|
60
|
+
`op read`.
|
|
61
|
+
|
|
62
|
+
### 1Password in detail
|
|
63
|
+
|
|
64
|
+
Two modes, chosen by the environment:
|
|
65
|
+
|
|
66
|
+
- **Service account** when `OP_SERVICE_ACCOUNT_TOKEN` is set. The SDK resolves
|
|
67
|
+
every reference in one round trip. No binary, no biometrics. For containers.
|
|
68
|
+
- **CLI** otherwise. Every reference goes through one `op inject`. If `op` has
|
|
69
|
+
no session, `envref` signs in: the account password comes from Keychain
|
|
70
|
+
behind Touch ID if you stored it with `envref op save-password`, or from the
|
|
71
|
+
terminal if not. The session token is cached in `~/.cache/envref/` so a
|
|
72
|
+
burst of commands costs one sign-in; concurrent processes share it through
|
|
73
|
+
a lock. See [SECURITY.md](SECURITY.md) for what that cache means.
|
|
74
|
+
|
|
75
|
+
Session caching presumes "biometric unlock for the CLI" is off in the
|
|
76
|
+
1Password app. With it on, `op` prompts per invocation and `envref` holds no
|
|
77
|
+
token.
|
|
78
|
+
|
|
79
|
+
Knobs: `OP_ACCOUNT` (shorthand, when several accounts are signed in),
|
|
80
|
+
`ENVREF_OP_SESSION_TTL` (seconds, default 1500, `0` disables the cache),
|
|
81
|
+
`ENVREF_OP_KEYCHAIN_SERVICE` / `ENVREF_OP_KEYCHAIN_ACCOUNT` (where the
|
|
82
|
+
password is stored, default `envref-op` / the account's email).
|
|
83
|
+
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
envref run [-f FILE]... [--override] -- cmd args # exec cmd with the resolved environment
|
|
88
|
+
envref export [-f FILE]... [--format shell|dotenv|json]
|
|
89
|
+
envref check [-f FILE]... # resolve and report per key; never prints a value
|
|
90
|
+
envref doctor # installed resolvers and whether each can run here
|
|
91
|
+
envref op save-password [--account SHORTHAND] # store the 1Password password in Keychain for Touch ID sign-in
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`-f` repeats. The first file to set a name wins, so `-f .env.refs -f .env`
|
|
95
|
+
lets a local `.env` fill in what the shared references file lacks without
|
|
96
|
+
overriding it. Default is `.env`.
|
|
97
|
+
|
|
98
|
+
## Library
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import envref
|
|
102
|
+
|
|
103
|
+
values = envref.load(".env.refs", ".env") # dict, also exported into os.environ
|
|
104
|
+
values = envref.load(".env", export=False) # dict only
|
|
105
|
+
envref.load(".env", override=True) # let the file win over existing variables
|
|
106
|
+
|
|
107
|
+
entries = envref.parse(text) # list[Entry], refs identified
|
|
108
|
+
envref.resolve(entries, registry={"op": ...}) # bring your own resolvers
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Errors are typed: `UnknownScheme`, `ResolverUnavailable` (the backend cannot
|
|
112
|
+
run here: no `op`, not macOS, no token) and `ResolveError` (which references
|
|
113
|
+
failed and why, in `.failures`). All derive from `EnvrefError`.
|
|
114
|
+
|
|
115
|
+
## Writing a resolver
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# myenvref/aws.py
|
|
119
|
+
class SecretsManagerResolver:
|
|
120
|
+
scheme = "awssm"
|
|
121
|
+
|
|
122
|
+
def resolve(self, refs: list[str]) -> dict[str, str]:
|
|
123
|
+
# refs are every awssm:// reference in the file, deduplicated: batch them.
|
|
124
|
+
...
|
|
125
|
+
return {ref: value for ref, value in ...}
|
|
126
|
+
|
|
127
|
+
def doctor(self) -> str: # optional, shown by `envref doctor`
|
|
128
|
+
return "ok"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```toml
|
|
132
|
+
# pyproject.toml
|
|
133
|
+
[project.entry-points."envref.resolvers"]
|
|
134
|
+
awssm = "myenvref.aws:SecretsManagerResolver"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Installed next to `envref`, `{{ awssm://… }}` now resolves. An entry point
|
|
138
|
+
with a built-in's name replaces it.
|
|
139
|
+
|
|
140
|
+
## Prior art
|
|
141
|
+
|
|
142
|
+
[`teller`](https://github.com/tellerops/teller) and
|
|
143
|
+
[`novops`](https://github.com/PierreBeucher/novops) are multi-provider too,
|
|
144
|
+
with the mapping in a YAML config rather than inline in the `.env`.
|
|
145
|
+
[`berglas`](https://github.com/GoogleCloudPlatform/berglas) has inline
|
|
146
|
+
`sm://` references, for Google Cloud only. [`dotenvx`](https://dotenvx.com)
|
|
147
|
+
encrypts the values rather than referencing them. `op inject` is what the
|
|
148
|
+
`{{ }}` syntax comes from, and any `.env.op` written for it is a valid
|
|
149
|
+
`envref` file.
|
|
150
|
+
|
|
151
|
+
## Status
|
|
152
|
+
|
|
153
|
+
Extracted in November 2026 from a private automation repository where the
|
|
154
|
+
1Password path had run daily for a year, on a laptop and in scheduled
|
|
155
|
+
containers. The other resolvers are new. macOS and Linux; Keychain and Touch
|
|
156
|
+
ID are macOS only.
|
|
157
|
+
|
|
158
|
+
MIT.
|
envref-0.1.0/SECURITY.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Threat model
|
|
4
|
+
|
|
5
|
+
`envref` moves secrets from a store into a process's environment. It never
|
|
6
|
+
stores a secret itself, with one deliberate exception described below. What
|
|
7
|
+
you should know before trusting it:
|
|
8
|
+
|
|
9
|
+
**The file is not secret.** An `.env` of references holds the names of your
|
|
10
|
+
vaults and items, which is an inventory of what you use. Treat it like source
|
|
11
|
+
code: fine in a private repository, think twice in a public one.
|
|
12
|
+
|
|
13
|
+
**Resolved values live in the environment of the process you start.** Any
|
|
14
|
+
child process inherits them, and on most systems any process running as the
|
|
15
|
+
same user can read another process's environment. This is true of every
|
|
16
|
+
secrets-to-env tool; `envref` adds nothing and removes nothing here.
|
|
17
|
+
|
|
18
|
+
**The 1Password CLI resolver caches a session token.** With "biometric unlock
|
|
19
|
+
for the CLI" turned off in 1Password, `op signin` returns a session token that
|
|
20
|
+
is good for about 30 minutes of inactivity. `envref` writes it to
|
|
21
|
+
`$XDG_CACHE_HOME/envref/op-session.json` (default `~/.cache/envref/`), mode
|
|
22
|
+
0600, and re-uses it so a burst of commands costs one sign-in rather than one
|
|
23
|
+
per command. The trade: during that window, any process running as your user
|
|
24
|
+
can read the token and, with it, every item the account can see, with no
|
|
25
|
+
Touch ID. If that is unacceptable for your machine, set
|
|
26
|
+
`ENVREF_OP_SESSION_TTL=0` to disable the cache, or turn biometric CLI unlock
|
|
27
|
+
on in 1Password, in which case `op` prompts per invocation and `envref` never
|
|
28
|
+
holds a token.
|
|
29
|
+
|
|
30
|
+
**The 1Password account password may live in macOS Keychain.** `envref op
|
|
31
|
+
save-password` stores it as a generic password item, so that an expired
|
|
32
|
+
session can be renewed without typing. Reading it goes through a small Swift
|
|
33
|
+
helper that requires Touch ID (`LAContext`, biometrics only) before calling
|
|
34
|
+
`security find-generic-password`. Without Touch ID hardware, or without
|
|
35
|
+
`swiftc`, the read falls back to `security` alone, which shows the system's
|
|
36
|
+
own authorisation dialog. The helper is compiled from source on first use
|
|
37
|
+
into the cache directory; it is not shipped as a binary.
|
|
38
|
+
|
|
39
|
+
**The service-account path holds a bearer token in the environment.** That is
|
|
40
|
+
how 1Password service accounts work. Scope the account to one vault that holds
|
|
41
|
+
only what the job needs, so a leaked token reads five items rather than every
|
|
42
|
+
credential you own. `envref` fails loudly when a reference points outside the
|
|
43
|
+
granted vault rather than falling back to another method.
|
|
44
|
+
|
|
45
|
+
**Resolvers are code.** A third-party resolver installed through the
|
|
46
|
+
`envref.resolvers` entry point runs with your privileges and sees every
|
|
47
|
+
reference of its scheme. Install resolvers the way you install any dependency.
|
|
48
|
+
|
|
49
|
+
## Reporting
|
|
50
|
+
|
|
51
|
+
Open a private security advisory on GitHub, or email the address on the
|
|
52
|
+
author's GitHub profile. Please do not open a public issue for a vulnerability.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "envref"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A .env where any value can be a reference into a secret store, resolved at load time. op://, keychain://, file://, env://, and your own."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Sebastian Haglund" }]
|
|
9
|
+
keywords = ["dotenv", "secrets", "1password", "keychain", "environment", "twelve-factor"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# 1Password service accounts (unattended, no `op` binary). The CLI path needs
|
|
29
|
+
# nothing but the `op` binary on PATH.
|
|
30
|
+
op = ["onepassword-sdk>=0.3; python_version < '3.14'"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/sebaheg/envref"
|
|
34
|
+
Repository = "https://github.com/sebaheg/envref"
|
|
35
|
+
Issues = "https://github.com/sebaheg/envref/issues"
|
|
36
|
+
Changelog = "https://github.com/sebaheg/envref/blob/main/CHANGELOG.md"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
envref = "envref.cli:main"
|
|
40
|
+
|
|
41
|
+
# Third-party resolvers register here: `name` is the URL scheme, the value a
|
|
42
|
+
# `module:Class` whose instances have `.resolve(refs) -> dict[str, str]`.
|
|
43
|
+
[project.entry-points."envref.resolvers"]
|
|
44
|
+
env = "envref.resolvers.env:EnvResolver"
|
|
45
|
+
file = "envref.resolvers.file:FileResolver"
|
|
46
|
+
keychain = "envref.resolvers.keychain:KeychainResolver"
|
|
47
|
+
op = "envref.resolvers.op:OnePasswordResolver"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = ["pytest>=8"]
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["hatchling"]
|
|
54
|
+
build-backend = "hatchling.build"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/envref"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = ["src/envref", "tests", "README.md", "LICENSE", "CHANGELOG.md", "SECURITY.md"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""envref — a .env where any value can be a reference into a secret store.
|
|
2
|
+
|
|
3
|
+
ATTIO_KEY={{ op://vault/Attio/api_key }}
|
|
4
|
+
DB_PASSWORD={{ keychain://myapp/db }}
|
|
5
|
+
TLS_KEY={{ file://~/.ssh/server.pem }}
|
|
6
|
+
REGION=eu-north-1
|
|
7
|
+
|
|
8
|
+
>>> import envref
|
|
9
|
+
>>> envref.load(".env") # resolves every reference, exports the rest
|
|
10
|
+
|
|
11
|
+
The scheme of a reference picks the resolver. Four ship built in (`op`,
|
|
12
|
+
`keychain`, `file`, `env`); others register through the `envref.resolvers`
|
|
13
|
+
entry-point group.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from envref.core import (
|
|
17
|
+
EnvrefError,
|
|
18
|
+
Resolver,
|
|
19
|
+
ResolveError,
|
|
20
|
+
ResolverUnavailable,
|
|
21
|
+
UnknownScheme,
|
|
22
|
+
load,
|
|
23
|
+
resolve,
|
|
24
|
+
)
|
|
25
|
+
from envref.parse import Entry, parse
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Entry",
|
|
31
|
+
"EnvrefError",
|
|
32
|
+
"ResolveError",
|
|
33
|
+
"Resolver",
|
|
34
|
+
"ResolverUnavailable",
|
|
35
|
+
"UnknownScheme",
|
|
36
|
+
"__version__",
|
|
37
|
+
"load",
|
|
38
|
+
"parse",
|
|
39
|
+
"resolve",
|
|
40
|
+
]
|