configre 2.1.4 → 2.1.5
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.
- package/README.md +158 -212
- package/demo/config/hostname.cjs +0 -0
- package/demo/config/index.cjs +0 -0
- package/demo/secrets/config/index.cjs +3 -3
- package/index.js +27 -1
- package/package.json +6 -7
- package/skills/configre/SKILL.md +90 -10
- package/test/index.test.js +69 -3
package/README.md
CHANGED
|
@@ -1,151 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Configre
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Define your defaults once. Let each environment describe what changes. Keep secrets alongside the settings they belong to.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Configre loads a base configuration, merges in a host or profile, and returns a plain JavaScript object. Configuration lives in `.cjs` files, so you get nested objects, comments, and ordinary JavaScript. Optional secret files follow the same structure and can be shared with your servers through encrypted files in Git.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Fallback to Defaults**: Uses a default configuration as a baseline, ensuring your application always has the necessary settings.
|
|
9
|
-
- **Easy Integration**: A simple setup process that integrates effortlessly into any project.
|
|
10
|
-
- **Support for `.cjs` Config Files Only**: Config files must use the `.cjs` extension. This allows dynamic configuration values and comments, and works the same in both CommonJS and ESM projects.
|
|
7
|
+
## Quick start
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
Requires **Node.js 22.13 or later**.
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Configre requires **Node.js 22.13 or later**. Add it to your project:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm install configre --save
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
> You can also add Configre as a skill for AI agentic development:
|
|
25
|
-
> ```bash
|
|
26
|
-
> npx skills add https://github.com/clasen/Configre --skill configre
|
|
27
|
-
> ```
|
|
28
|
-
|
|
29
|
-
2. **Setup Your Configuration Files**
|
|
30
|
-
|
|
31
|
-
Organize your configuration files within a directory (e.g., `config`). Create a default configuration file and environment-specific files as needed.
|
|
32
|
-
|
|
33
|
-
- `index.cjs`: Your default configuration.
|
|
34
|
-
- `[hostname].cjs`: Override configurations for specific hosts.
|
|
35
|
-
- `[hostname].dev.cjs`: Development-specific configurations.
|
|
36
|
-
|
|
37
|
-
> **Note:** Config files must always use the `.cjs` extension; `.js` config files are not accepted. That way they work in both CommonJS and ESM projects and you can use `module.exports` in them.
|
|
38
|
-
|
|
39
|
-
3. **Use Configre in Your Project**
|
|
40
|
-
|
|
41
|
-
Import and use Configre to load your configurations. The configuration path is required:
|
|
11
|
+
```bash
|
|
12
|
+
npm install configre
|
|
13
|
+
```
|
|
42
14
|
|
|
43
|
-
|
|
44
|
-
import Configre from "configre";
|
|
45
|
-
import { join } from "node:path";
|
|
15
|
+
Create `config/index.cjs` with your defaults:
|
|
46
16
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
17
|
+
```javascript
|
|
18
|
+
module.exports = {
|
|
19
|
+
db: {
|
|
20
|
+
host: "localhost",
|
|
21
|
+
port: 5432,
|
|
22
|
+
password: ""
|
|
23
|
+
},
|
|
24
|
+
api: {
|
|
25
|
+
key: ""
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
```
|
|
50
29
|
|
|
51
|
-
|
|
30
|
+
Load it from `app.mjs`, next to the `config` directory:
|
|
52
31
|
|
|
53
|
-
|
|
32
|
+
```javascript
|
|
33
|
+
import Configre from "configre";
|
|
34
|
+
import { join } from "node:path";
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
const cfg = Configre(join(import.meta.dirname, "config"));
|
|
37
|
+
console.log(cfg.db.port); // 5432
|
|
38
|
+
```
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
|
|
40
|
+
```bash
|
|
41
|
+
node app.mjs
|
|
42
|
+
```
|
|
61
43
|
|
|
62
|
-
|
|
44
|
+
The configuration path is required. Anchor it to your module so starting the process from another directory does not change which configuration it loads. Avoid building this path from `process.cwd()`.
|
|
63
45
|
|
|
64
|
-
|
|
46
|
+
Configre is a native ES module. CommonJS consumers can also use the synchronous API: `require("configre")` returns the function directly. Configuration files use `.cjs` and `module.exports` in either kind of project.
|
|
65
47
|
|
|
66
|
-
|
|
48
|
+
## Configuration that builds on defaults
|
|
67
49
|
|
|
68
|
-
|
|
69
|
-
- `myhostname.cjs`: Contains overrides for the host named `myhostname`.
|
|
50
|
+
An environment should describe what changes. Everything else comes from the base.
|
|
70
51
|
|
|
71
|
-
|
|
52
|
+
Add `config/production.cjs`:
|
|
72
53
|
|
|
73
54
|
```javascript
|
|
74
55
|
module.exports = {
|
|
75
56
|
db: {
|
|
76
|
-
host:
|
|
77
|
-
|
|
78
|
-
password: 'mypassword'
|
|
79
|
-
},
|
|
80
|
-
api: {
|
|
81
|
-
key: 'development-api-key'
|
|
57
|
+
host: "db.internal",
|
|
58
|
+
ssl: true
|
|
82
59
|
}
|
|
83
|
-
}
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Select that profile:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node app.mjs --config=production
|
|
84
67
|
```
|
|
85
68
|
|
|
86
|
-
|
|
69
|
+
The resulting configuration contains:
|
|
87
70
|
|
|
88
71
|
```javascript
|
|
89
|
-
|
|
72
|
+
{
|
|
90
73
|
db: {
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
host: "db.internal",
|
|
75
|
+
port: 5432,
|
|
76
|
+
password: "",
|
|
77
|
+
ssl: true
|
|
78
|
+
},
|
|
79
|
+
api: {
|
|
80
|
+
key: ""
|
|
93
81
|
}
|
|
94
82
|
}
|
|
95
83
|
```
|
|
96
84
|
|
|
97
|
-
|
|
85
|
+
`host` is overwritten, `port` is inherited, and `ssl` is added. The `api` object stays intact. A profile can extend nested objects without repeating their other fields.
|
|
98
86
|
|
|
99
|
-
|
|
87
|
+
### Selecting a profile
|
|
100
88
|
|
|
101
|
-
By default Configre uses the machine
|
|
89
|
+
By default, Configre uses the machine's hostname. A machine named `web-01` loads `config/web-01.cjs`. Pass `--config=<profile>` to choose a name explicitly; the flag can appear alongside other application arguments.
|
|
102
90
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
| File | Role |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `index.cjs` | Base configuration |
|
|
94
|
+
| `<profile>.cjs` | Additions and overrides for the selected profile |
|
|
95
|
+
| `<profile>.dev.cjs` | Preferred over `<profile>.cjs` when present |
|
|
108
96
|
|
|
109
|
-
|
|
97
|
+
If no profile file exists, Configre uses the base configuration. When `production.dev.cjs` exists, it is selected **instead of** `production.cjs`; those two files do not merge. This selection depends on the files present, not on `NODE_ENV`.
|
|
110
98
|
|
|
111
|
-
|
|
99
|
+
Objects merge recursively. Later values override matching fields. Arrays merge **by index**, rather than being replaced as a whole: `["a", "b"]` followed by `["c"]` becomes `["c", "b"]`.
|
|
112
100
|
|
|
113
|
-
|
|
101
|
+
## Secrets, organized like your configuration
|
|
114
102
|
|
|
115
|
-
|
|
103
|
+
Database credentials belong under `db`. API keys belong under `api`. Secret files keep that organization while separating sensitive values from public settings.
|
|
116
104
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
```text
|
|
106
|
+
config/
|
|
107
|
+
index.cjs # Shared public settings
|
|
108
|
+
production.cjs # Production additions and overrides
|
|
109
|
+
index.secret.cjs # Shared secrets, edited locally
|
|
110
|
+
production.secret.cjs # Production secrets, edited locally
|
|
123
111
|
```
|
|
124
112
|
|
|
125
|
-
|
|
113
|
+
A secret file exports only the fields it needs to supply. Your application still reads `cfg.db.password` and `cfg.api.key`; there is no separate secrets API.
|
|
126
114
|
|
|
127
|
-
|
|
115
|
+
For `--config=production`, the merge order is:
|
|
128
116
|
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
cfg.print();
|
|
117
|
+
```text
|
|
118
|
+
index.cjs → production.cjs → index.secret.cjs → production.secret.cjs
|
|
132
119
|
```
|
|
133
120
|
|
|
134
|
-
`
|
|
135
|
-
It omits fields supplied by the base and selected profile's `.secret.cjs` files,
|
|
136
|
-
including when loaded from the encrypted file. Public siblings in nested objects
|
|
137
|
-
remain visible; arrays supplied by secrets are omitted entirely. It does not
|
|
138
|
-
modify the configuration or log automatically on load. `cfg.print()` logs its
|
|
139
|
-
current values, including changes made after loading. The method is non-enumerable
|
|
140
|
-
and does not appear in `Object.keys(cfg)`, object spreads or JSON output.
|
|
141
|
-
`new Configre(configPath).print()` is also supported. If your configuration already
|
|
142
|
-
has a field named `print`, that value is preserved; use the constructor API to print it.
|
|
143
|
-
Sensitivity is determined by secret-file fields, not by names such as `password`
|
|
144
|
-
or `token`: keep sensitive values in `.secret.cjs` files, not in public settings.
|
|
121
|
+
Later layers win. Shared secrets can be extended or overwritten by profile secrets, just like public settings. Secret selection independently prefers `production.dev.secret.cjs` over `production.secret.cjs`; it does not merge the two variants.
|
|
145
122
|
|
|
146
|
-
###
|
|
123
|
+
### Add your secrets
|
|
147
124
|
|
|
148
|
-
|
|
125
|
+
Create `config/index.secret.cjs` for shared values:
|
|
149
126
|
|
|
150
127
|
```javascript
|
|
151
128
|
module.exports = {
|
|
@@ -155,175 +132,144 @@ module.exports = {
|
|
|
155
132
|
};
|
|
156
133
|
```
|
|
157
134
|
|
|
158
|
-
|
|
135
|
+
For a production database credential, create `config/production.secret.cjs`:
|
|
159
136
|
|
|
160
137
|
```javascript
|
|
161
|
-
module.exports = {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
node demo/secrets/demo.js
|
|
138
|
+
module.exports = {
|
|
139
|
+
db: {
|
|
140
|
+
password: ""
|
|
141
|
+
}
|
|
142
|
+
};
|
|
168
143
|
```
|
|
169
144
|
|
|
170
|
-
|
|
145
|
+
Fill in the values in these local files. Configre generates `secrets.enc.json` and adds Git exclusions for the editable secret files when your application runs.
|
|
171
146
|
|
|
172
|
-
|
|
173
|
-
demo/secrets/config/
|
|
174
|
-
index.cjs # Public configuration
|
|
175
|
-
index.secret.cjs # Editable secrets you create yourself
|
|
176
|
-
recipients/ # Public keys of servers that register
|
|
177
|
-
secrets.enc.json # Generated encrypted values
|
|
178
|
-
.gitignore # Keeps .secret.cjs files out of Git
|
|
179
|
-
```
|
|
147
|
+
### Share with a server
|
|
180
148
|
|
|
181
|
-
|
|
149
|
+
1. **Local:** run your application and push the generated encrypted file.
|
|
150
|
+
2. **Server:** pull and run. On the first run, Configre automatically commits and pushes the server's public key as `recipients/<profile>.pub` (or `<hostname>.pub` when no profile is specified).
|
|
151
|
+
3. **Local:** pull, run again to include that public key in the encrypted file, and push.
|
|
152
|
+
4. **Server:** pull and restart. The secrets are now available through `cfg`.
|
|
182
153
|
|
|
183
|
-
|
|
154
|
+
The server needs Git push access for registration. Until the updated encrypted file arrives, Configre loads public settings only.
|
|
184
155
|
|
|
185
|
-
|
|
186
|
-
API key configured: false
|
|
187
|
-
```
|
|
156
|
+
To update secrets later, edit them locally, run, and push. Pull and restart on the server. Registration happens only once per identity.
|
|
188
157
|
|
|
189
|
-
**
|
|
158
|
+
**Profiles organize secrets; they do not isolate access.** Authorized identities can decrypt all profiles in the encrypted file. Pulling a new registration and publishing the updated encrypted file grants that identity access.
|
|
190
159
|
|
|
191
|
-
|
|
160
|
+
## Utilities
|
|
192
161
|
|
|
193
|
-
###
|
|
162
|
+
### Inspect configuration with `cfg.print()`
|
|
194
163
|
|
|
195
|
-
|
|
164
|
+
The returned configuration contains decrypted secrets. Use `cfg.print()` when inspecting it:
|
|
196
165
|
|
|
197
166
|
```javascript
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
key: ""
|
|
201
|
-
}
|
|
202
|
-
};
|
|
167
|
+
const cfg = Configre(join(import.meta.dirname, "config"));
|
|
168
|
+
cfg.print();
|
|
203
169
|
```
|
|
204
170
|
|
|
205
|
-
|
|
171
|
+
Enable its debug output with:
|
|
206
172
|
|
|
207
173
|
```bash
|
|
208
|
-
node
|
|
174
|
+
DEBUG=Configre:* node app.mjs --config=production
|
|
209
175
|
```
|
|
210
176
|
|
|
211
|
-
|
|
177
|
+
`print()` omits fields supplied by the base and selected profile's secret files, including when those values come from the encrypted file. Public siblings remain visible; arrays supplied by secrets are omitted entirely. Omission is based on secret-file fields, not names such as `password` or `token`. Keep sensitive values in secret files.
|
|
212
178
|
|
|
213
|
-
|
|
214
|
-
API key configured: true
|
|
215
|
-
```
|
|
179
|
+
It logs current values, including edits made after loading, without changing the configuration. It does not log automatically on load. The method is non-enumerable, so it stays out of `Object.keys(cfg)`, spreads, and JSON output. Spreads and JSON output still include the configuration's secret values.
|
|
216
180
|
|
|
217
|
-
|
|
181
|
+
The constructor API is also available:
|
|
218
182
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
| `recipients/truco.pub` | Public identity of the server named `truco` | Yes; the server publishes it |
|
|
225
|
-
| `.gitignore` | Excludes editable secret files | Yes |
|
|
183
|
+
```javascript
|
|
184
|
+
const config = new Configre(join(import.meta.dirname, "config"));
|
|
185
|
+
const cfg = config.get();
|
|
186
|
+
config.print();
|
|
187
|
+
```
|
|
226
188
|
|
|
227
|
-
|
|
189
|
+
If your configuration already has a field named `print`, Configre preserves it. Use the constructor API to print in that case.
|
|
228
190
|
|
|
229
|
-
|
|
191
|
+
### Apply environment variables explicitly
|
|
230
192
|
|
|
231
|
-
|
|
193
|
+
Use `applyConfigEnv(cfg.env)` when a library reads its settings from `process.env`:
|
|
232
194
|
|
|
233
|
-
```
|
|
234
|
-
|
|
195
|
+
```javascript
|
|
196
|
+
import Configre, { applyConfigEnv } from "configre";
|
|
197
|
+
import { join } from "node:path";
|
|
198
|
+
|
|
199
|
+
const cfg = Configre(join(import.meta.dirname, "config"));
|
|
200
|
+
applyConfigEnv(cfg.env);
|
|
235
201
|
```
|
|
236
202
|
|
|
237
|
-
|
|
203
|
+
For CommonJS, use `const { applyConfigEnv } = require("configre")`.
|
|
238
204
|
|
|
239
|
-
|
|
205
|
+
The helper converts values with `String(value)` and sets only variables that are undefined in `process.env`. Existing values, including empty strings, are preserved. An undefined `cfg.env` does nothing; null or undefined entries are skipped.
|
|
240
206
|
|
|
241
|
-
|
|
242
|
-
2. Writes `demo/secrets/config/recipients/truco.pub`.
|
|
243
|
-
3. Creates and pushes a commit containing only that public-key file.
|
|
244
|
-
4. Logs a warning and continues with public configuration only until the administrator publishes an updated encrypted file.
|
|
207
|
+
It rejects null, arrays and non-objects as `cfg.env`, empty keys, and object-valued entries. Entries are processed in order; an invalid entry throws without undoing previous assignments. Loading Configre does not call this helper automatically, and empty placeholders are never filled from environment variables automatically.
|
|
245
208
|
|
|
246
|
-
|
|
209
|
+
## Advanced reference
|
|
247
210
|
|
|
248
|
-
|
|
211
|
+
<details>
|
|
212
|
+
<summary>Secret formats, identities, Git behavior, and recovery</summary>
|
|
249
213
|
|
|
250
|
-
###
|
|
214
|
+
### Secret files and loading
|
|
251
215
|
|
|
252
|
-
|
|
216
|
+
Secret `.cjs` modules execute locally on the machine where you edit them and are reloaded on each Configre call. They must export plain objects containing JSON-compatible objects, arrays, strings, finite numbers, booleans, and `null`. Functions, `undefined`, accessors, symbols, custom objects, circular references, and properties named `__proto__`, `constructor`, or `prototype` are rejected. Servers decrypt data without executing these modules.
|
|
253
217
|
|
|
254
|
-
|
|
255
|
-
git pull --ff-only
|
|
256
|
-
node demo/secrets/demo.js
|
|
257
|
-
```
|
|
218
|
+
Secrets activate when the base or selected profile has a secret counterpart, or when the encrypted file already exists. Without either, loading public configuration creates no secret artifacts. Once active, all local profile secret files are encrypted together, including inactive profiles.
|
|
258
219
|
|
|
259
|
-
|
|
220
|
+
A selected profile's secret file can activate secrets without `index.secret.cjs`. A public profile alone never creates a secret counterpart. A missing profile uses the available base settings and base secrets. A checkout with an encrypted file and no editable secret files acts as a consumer. Existing ciphertext is read and validated before updates; it is not reset on startup. There is no `secrets` option, and function, constructor, ESM, and CommonJS usage share the same behavior.
|
|
260
221
|
|
|
261
|
-
|
|
222
|
+
For an individual configuration file, sidecars live beside it: `settings.secret.cjs`, `settings.cjs.recipients/`, and `settings.cjs.secrets.enc.json`. An extensionless path resolving to `settings.cjs` uses the same sidecars.
|
|
262
223
|
|
|
263
|
-
|
|
264
|
-
git pull --ff-only
|
|
265
|
-
node demo/secrets/demo.js --config=truco
|
|
266
|
-
```
|
|
224
|
+
### Identities and registration
|
|
267
225
|
|
|
268
|
-
|
|
226
|
+
Automatic registration requires a Git checkout with a configured upstream, Git author name/email, and credentials that allow a non-interactive push. The branch must match its upstream before a new registration. Failed registrations are retried on the next load.
|
|
269
227
|
|
|
270
|
-
|
|
228
|
+
Each OS user has one identity, stored at `~/.config/configre/identity.pem` and `identity.pub`, reused across projects. Authorization is per project. A service running under another OS user needs its own registration.
|
|
271
229
|
|
|
272
|
-
|
|
230
|
+
Recipient files must contain a single RSA-3072 public key in PEM format, with exponent 65537, as generated by Configre. Other extensions are ignored, and duplicate keys do not add recipients. The identity encrypting the file is always included.
|
|
273
231
|
|
|
274
|
-
|
|
232
|
+
Registration names must start with a letter or digit and contain only letters, digits, dots, underscores, or hyphens. If another key already occupies `production.pub`, registration stops without overwriting it, and configuration loading continues with a warning and public settings only. Use a distinct profile or resolve the key replacement explicitly.
|
|
275
233
|
|
|
276
|
-
|
|
277
|
-
node demo/secrets/demo.js
|
|
278
|
-
```
|
|
234
|
+
Registration uses an isolated Git index and publishes only the public-key file. It preserves unrelated staged and unstaged changes, runs no commit or pre-push hooks, and does not push local tags, merge, rebase, or force-push. Repeated loads do not publish another registration commit for the same key. Each Git command has a 30-second timeout. An authorized server loads secrets without Git commands or project writes.
|
|
279
235
|
|
|
280
|
-
|
|
236
|
+
Configre appends Git exclusions without removing existing rules, including when no repository exists yet. It refuses to proceed on the machine holding editable secrets if those modules are already tracked. It does not untrack files or rewrite history.
|
|
281
237
|
|
|
282
|
-
|
|
238
|
+
### Updates, revocation, and recovery
|
|
283
239
|
|
|
284
|
-
|
|
240
|
+
The editable `.secret.cjs` files are the source of truth for values; `recipients/` is the source of truth for authorization. To revoke a server, remove its public-key file, run Configre locally, and commit and push both the removal and the updated encrypted file. Remove the server's repository write access too if it must not register again. Revocation protects new encrypted versions. Rotate credentials at their providers to invalidate values a server already received.
|
|
285
241
|
|
|
286
|
-
|
|
287
|
-
index.cjs → truco.cjs → index.secret.cjs → truco.secret.cjs
|
|
288
|
-
```
|
|
242
|
+
Removing all recipient files revokes them on the next local load; old ciphertext does not restore them. Recover accidentally deleted public-key files from Git before reloading. Missing Git exclusions and ciphertext are regenerated on the machine holding editable secrets, and existing secret modules are never overwritten.
|
|
289
243
|
|
|
290
|
-
|
|
244
|
+
Back up editable secrets and the private identity securely. Never share or commit `identity.pem`. On POSIX, the identity directory must have owner-only permissions (`0700`), and the private file must have owner-only permissions (`0600`). On Windows, protection depends on the user profile's filesystem permissions. Private identities and secret input files cannot be symlinks.
|
|
291
245
|
|
|
292
|
-
|
|
246
|
+
A corrupt or missing private identity is never silently replaced. Restore it from backup, or register a new identity on a consumer. A missing public file can be regenerated from the valid private key. Losing every authorized private key makes existing ciphertext unrecoverable; surviving editable secrets can be used to create a fresh encrypted file in a new setup.
|
|
293
247
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- Registration names must start with a letter or digit and contain only letters, digits, dots, underscores or hyphens. If another key already occupies `truco.pub`, registration stops without overwriting it, and configuration loading continues with a warning and public settings only; use a distinct profile or resolve the key replacement explicitly.
|
|
304
|
-
- Configre appends Git exclusions without removing existing rules and refuses to proceed on the administrator if editable secret modules are already tracked. It does not untrack files or rewrite history. It also prepares `.gitignore` when no repository exists yet.
|
|
305
|
-
- The administrator's `.secret.cjs` files are the source of truth for values, and `recipients/` is the source of truth for authorization. Removing all recipient files revokes them on the next administrator reload; old ciphertext does not restore them. Recover accidentally deleted public-key files from Git before reloading. Missing Git exclusions and ciphertext are regenerated on the administrator, and existing secret modules are never overwritten.
|
|
306
|
-
- Back up the administrator's editable secrets and private identity securely. Never share or commit `identity.pem`. On POSIX, its directory must have owner-only permissions (`0700`) and the private file must have owner-only permissions (`0600`); on Windows, protection depends on the user profile's filesystem permissions. Private identities and secret input files cannot be symlinks.
|
|
307
|
-
- A corrupt or missing private identity is never silently replaced. Restore it from backup, or register a new identity on a consumer. A missing public file can be regenerated from the valid private key. Losing every authorized private key makes existing ciphertext unrecoverable; an administrator with surviving editable secret files can create fresh ciphertext.
|
|
308
|
-
- The encrypted JSON envelope uses AES-256-GCM with a fresh content key and nonce for each update, wrapping that key for each recipient with RSA-3072 / OAEP-SHA-256. Version and recipient metadata are authenticated. Invalid modules, malformed ciphertext and authentication failures stop loading without replacing the existing encrypted file.
|
|
309
|
-
- Ciphertext is replaced atomically under an exclusive writer lock. Temporary vault files contain only encrypted data and public metadata. After a crash, remove a reported stale `.lock` only after confirming its writer has stopped. Unchanged values and recipient keys do not cause a rewrite.
|
|
310
|
-
- For an individual configuration file, the same workflow uses sidecars beside that file: `settings.secret.cjs`, `settings.cjs.recipients/` and `settings.cjs.secrets.enc.json`. An extensionless path resolving to `settings.cjs` uses the same sidecars.
|
|
248
|
+
### Encryption and writes
|
|
249
|
+
|
|
250
|
+
The encrypted JSON envelope uses AES-256-GCM with a fresh content key and nonce for each update. That key is wrapped for each recipient with RSA-3072 / OAEP-SHA-256. Version and recipient metadata are authenticated. Invalid modules, malformed ciphertext, and authentication failures stop loading without replacing the existing encrypted file.
|
|
251
|
+
|
|
252
|
+
Ciphertext is replaced atomically under an exclusive writer lock. Temporary files contain only encrypted data and public metadata. After a crash, remove a reported stale `.lock` only after confirming its writer has stopped. Unchanged values and recipient keys do not cause a rewrite.
|
|
253
|
+
|
|
254
|
+
### Operational logs
|
|
255
|
+
|
|
256
|
+
With `DEBUG=Configre:*`, Configre reports identity creation, Git exclusions, recipient registration, encrypted-file updates, and successful public-key publication. These operation messages contain names and paths, never secret values or key contents. Unchanged files and previously published keys do not generate another creation or publication log.
|
|
311
257
|
|
|
312
258
|
</details>
|
|
313
259
|
|
|
314
|
-
##
|
|
260
|
+
## Resources and contributions
|
|
315
261
|
|
|
316
|
-
|
|
317
|
-
- **Clarity and Convenience**: Keep your configuration organized and easy to understand.
|
|
318
|
-
- **Flexibility**: Supports dynamic configuration values for complex setups.
|
|
262
|
+
Explore the [basic demo](https://github.com/clasen/Configre/blob/main/demo/demo.js), [ESM project demo](https://github.com/clasen/Configre/blob/main/demo/module/demo.js), or [secrets demo](https://github.com/clasen/Configre/blob/main/demo/secrets/demo.js).
|
|
319
263
|
|
|
320
|
-
|
|
264
|
+
To add the Configre skill to your coding agent:
|
|
321
265
|
|
|
322
|
-
|
|
266
|
+
```bash
|
|
267
|
+
npx skills add https://github.com/clasen/Configre --skill configre
|
|
268
|
+
```
|
|
323
269
|
|
|
324
|
-
|
|
270
|
+
Found a bug or a useful improvement? [Open an issue](https://github.com/clasen/Configre/issues) or submit a pull request. A small, reproducible example helps.
|
|
325
271
|
|
|
326
|
-
##
|
|
272
|
+
## License
|
|
327
273
|
|
|
328
274
|
The MIT License (MIT)
|
|
329
275
|
|
package/demo/config/hostname.cjs
CHANGED
|
File without changes
|
package/demo/config/index.cjs
CHANGED
|
File without changes
|
package/index.js
CHANGED
|
@@ -9,6 +9,30 @@ import loadSecrets from "./secrets/index.js";
|
|
|
9
9
|
const requireConfig = createRequire(import.meta.url);
|
|
10
10
|
const log = lemonlog("Configre");
|
|
11
11
|
|
|
12
|
+
function applyConfigEnv(env) {
|
|
13
|
+
if (env === undefined) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (env === null || typeof env !== "object" || Array.isArray(env)) {
|
|
17
|
+
throw new TypeError("config.env must be a plain object");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const [key, value] of Object.entries(env)) {
|
|
21
|
+
if (key.length === 0) {
|
|
22
|
+
throw new TypeError("config.env keys must be non-empty strings");
|
|
23
|
+
}
|
|
24
|
+
if (value == null) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "object") {
|
|
28
|
+
throw new TypeError(`config.env.${key} must be a scalar`);
|
|
29
|
+
}
|
|
30
|
+
if (process.env[key] === undefined) {
|
|
31
|
+
process.env[key] = String(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
function omitSecrets(settings, secrets) {
|
|
13
37
|
for (const [key, value] of Object.entries(secrets)) {
|
|
14
38
|
if (!Object.hasOwn(settings, key)) continue;
|
|
@@ -127,4 +151,6 @@ function Configre(path) {
|
|
|
127
151
|
}
|
|
128
152
|
}
|
|
129
153
|
|
|
130
|
-
|
|
154
|
+
Configre.applyConfigEnv = applyConfigEnv;
|
|
155
|
+
|
|
156
|
+
export { Configre as default, Configre as "module.exports", applyConfigEnv };
|
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configre",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "🔧 Effortlessly Tailor Your Settings",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.13.0"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node --test"
|
|
11
|
-
},
|
|
12
9
|
"dependencies": {
|
|
13
10
|
"lemonlog": "^1.2.2"
|
|
14
11
|
},
|
|
@@ -50,7 +47,6 @@
|
|
|
50
47
|
"url": "https://github.com/clasen/Configre/issues"
|
|
51
48
|
},
|
|
52
49
|
"homepage": "https://github.com/clasen/Configre#readme",
|
|
53
|
-
"packageManager": "pnpm@11.9.0",
|
|
54
50
|
"files": [
|
|
55
51
|
"index.js",
|
|
56
52
|
"merge.js",
|
|
@@ -66,5 +62,8 @@
|
|
|
66
62
|
"demo/module/config/hostname.cjs",
|
|
67
63
|
"demo/secrets/demo.js",
|
|
68
64
|
"demo/secrets/config/index.cjs"
|
|
69
|
-
]
|
|
70
|
-
|
|
65
|
+
],
|
|
66
|
+
"scripts": {
|
|
67
|
+
"test": "node --test"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/skills/configre/SKILL.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: configre
|
|
3
|
-
description: Set up and manage
|
|
3
|
+
description: Set up and manage Configre configuration in Node.js projects, including hostname/profile overrides, encrypted shared secrets, recipient registration, and configuration printing with secret fields omitted. Use when adopting Configre or working on its configuration files, secret sharing, or print() API.
|
|
4
4
|
metadata:
|
|
5
5
|
category: configuration
|
|
6
|
-
tags: [nodejs, config, environment, settings,
|
|
6
|
+
tags: [nodejs, config, environment, settings, secrets]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Configre
|
|
10
10
|
|
|
11
|
-
Environment-specific configuration manager for Node.js. Merges
|
|
11
|
+
Environment-specific configuration manager for Node.js. Merges public defaults, hostname or profile overrides, and optional secrets synchronously using its own deep-merge implementation.
|
|
12
12
|
|
|
13
13
|
## Instructions
|
|
14
14
|
|
|
@@ -40,15 +40,17 @@ module.exports = {
|
|
|
40
40
|
host: 'localhost',
|
|
41
41
|
port: 5432,
|
|
42
42
|
user: 'dev',
|
|
43
|
-
password:
|
|
43
|
+
password: ""
|
|
44
44
|
},
|
|
45
45
|
api: {
|
|
46
|
-
key:
|
|
46
|
+
key: "",
|
|
47
47
|
url: 'http://localhost:3000'
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Keep credentials in `.secret.cjs` files. Public settings should contain explicit empty placeholders, not real credentials or automatic environment-variable fallbacks.
|
|
53
|
+
|
|
52
54
|
### Step 4: Write host/profile overrides
|
|
53
55
|
|
|
54
56
|
Create `config/<hostname>.cjs` with only the keys that differ — they are deep-merged over defaults:
|
|
@@ -56,8 +58,7 @@ Create `config/<hostname>.cjs` with only the keys that differ — they are deep-
|
|
|
56
58
|
```javascript
|
|
57
59
|
module.exports = {
|
|
58
60
|
db: {
|
|
59
|
-
user: 'prod-user'
|
|
60
|
-
password: 'prod-secret'
|
|
61
|
+
user: 'prod-user'
|
|
61
62
|
}
|
|
62
63
|
};
|
|
63
64
|
```
|
|
@@ -90,7 +91,7 @@ Configre determines the active profile by looking for a `--config=<profile>` arg
|
|
|
90
91
|
|
|
91
92
|
1. `config/<profile>.dev.cjs` — dev override
|
|
92
93
|
2. `config/<profile>.cjs` — production override
|
|
93
|
-
3. No match — uses defaults
|
|
94
|
+
3. No match — uses public defaults; available secrets still apply
|
|
94
95
|
|
|
95
96
|
To force a specific profile at runtime:
|
|
96
97
|
|
|
@@ -101,6 +102,85 @@ node app.js --port=3000 --config=production --debug
|
|
|
101
102
|
|
|
102
103
|
Using `--config=` (instead of a positional argument) avoids conflicts with other CLI flags.
|
|
103
104
|
|
|
105
|
+
## Shared secrets
|
|
106
|
+
|
|
107
|
+
### Activation and file layout
|
|
108
|
+
|
|
109
|
+
Secret support activates when the base config or selected profile has a corresponding `.secret.cjs` file, or when an encrypted file already exists. There is no `secrets` option. Without either condition, Configre loads public settings without accessing an identity or creating secret artifacts. Configre never creates editable `.secret.cjs` modules itself; create them only when secret support is wanted.
|
|
110
|
+
|
|
111
|
+
For a config directory:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
config/
|
|
115
|
+
├── index.cjs
|
|
116
|
+
├── production.cjs
|
|
117
|
+
├── index.secret.cjs # Administrator-only base secrets
|
|
118
|
+
├── production.secret.cjs # Optional administrator-only profile secrets
|
|
119
|
+
├── production.dev.secret.cjs # Optional preferred secret profile variant
|
|
120
|
+
├── secrets.enc.json # Generated ciphertext; share through Git
|
|
121
|
+
├── recipients/*.pub # Public recipient keys; share through Git
|
|
122
|
+
└── .gitignore # Generated exclusions for editable secrets
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For an individual `settings.cjs` config file, sidecars are `settings.secret.cjs`, `settings.cjs.secrets.enc.json`, and `settings.cjs.recipients/`, beside the config file. An extensionless path resolving to that file uses the same sidecars. File mode selects the base secret module, not profile secret modules.
|
|
126
|
+
|
|
127
|
+
Secret modules export a plain object containing only JSON-compatible values. For example, create `config/index.secret.cjs` with empty values for the administrator to fill privately:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
module.exports = {
|
|
131
|
+
db: { password: "" },
|
|
132
|
+
api: { key: "" }
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Empty strings remain empty. Configre does not fill values from the environment or populate `process.env`. Functions, `undefined`, accessors, symbols, custom objects, circular references, non-finite numbers, sparse arrays, and keys named `__proto__`, `constructor`, or `prototype` are rejected in secret modules.
|
|
137
|
+
|
|
138
|
+
### Merge order
|
|
139
|
+
|
|
140
|
+
For `--config=production`, later layers override earlier ones:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
index.cjs → selected public profile → index.secret.cjs → selected secret profile
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Public selection prefers `production.dev.cjs` over `production.cjs`. Independently, secret selection prefers `production.dev.secret.cjs` over `production.secret.cjs`; dev and regular variants do not merge together. A selected profile secret can activate secrets without `index.secret.cjs`. Once activated, directory mode encrypts all local `.secret.cjs` files, including inactive profiles, while only base and selected profile values enter the returned config.
|
|
147
|
+
|
|
148
|
+
### Administrator and server workflow
|
|
149
|
+
|
|
150
|
+
1. On the administrator, create the needed `.secret.cjs` files and load Configre. It creates or reuses the machine identity, prepares Git exclusions and `recipients/`, and writes `secrets.enc.json`. Editable secrets must not already be tracked by Git; Configre refuses to proceed if they are. It does not untrack files or rewrite history.
|
|
151
|
+
2. Share the public configuration, generated `.gitignore`, recipient public keys, and ciphertext through Git. Keep editable secret modules and the private identity local. A server with ciphertext and no editable secret modules decrypts the bundle without executing secret modules.
|
|
152
|
+
3. On an unauthorized server, loading Configre attempts to publish `recipients/<profile>.pub` through Git. New registration requires a branch matching its upstream, Git author identity, and credentials that work without an interactive prompt. Profile names must start with a letter or digit and contain only letters, digits, dots, underscores, or hyphens.
|
|
153
|
+
4. On the administrator, pull the public-key registration, reload Configre, and publish the updated ciphertext. The server then pulls it and reloads Configre. Publishing a public key alone does not authorize decryption. Any authorized recipient can decrypt the whole bundle, including other profiles; profiles are not access-control boundaries.
|
|
154
|
+
|
|
155
|
+
Loading an unauthorized server config can fetch, create a public-key commit, and push it. Account for these side effects before using application startup as a verification command; use isolated fixtures for local checks unless the requested work authorizes live registration. Configre preserves unrelated staged and unstaged changes through an isolated index, runs no commit or pre-push hooks, and does not merge, rebase, force-push, or publish local tags. Already published matching keys do not create another registration commit. Conflicting keys for the same profile are not overwritten. Failed registration is retried on a later load; each registration Git command has a 30-second timeout.
|
|
156
|
+
|
|
157
|
+
While authorization is pending, Configre warns and returns public settings only: public placeholders remain and encrypted-only fields are absent. Registration failure also permits public-only startup. Do not assume successful application startup proves secrets were loaded; verify required credentials without printing their values. Invalid secret modules, damaged identities, malformed ciphertext, and authentication failures stop loading instead of silently resetting data.
|
|
158
|
+
|
|
159
|
+
### Updates, revocation, and identity
|
|
160
|
+
|
|
161
|
+
- The administrator's local `.secret.cjs` files are authoritative for values; `recipients/` is authoritative for additional recipients. Reloading updates ciphertext when values or recipients change. Unchanged inputs do not rewrite it; existing editable modules are never overwritten. Deleting every local secret module while retaining ciphertext switches to consumer behavior rather than clearing the bundle.
|
|
162
|
+
- To revoke a recipient, remove its public-key file on the administrator, reload, and distribute the new ciphertext. The current administrator is always included. Remove repository write access too if the machine must not register again. Old ciphertext and previously obtained credentials remain usable; rotate affected credentials when revocation requires it.
|
|
163
|
+
- Identity files live at `~/.config/configre/identity.pem` (private) and `identity.pub` (public). Share only the public key. Back up editable secrets and the private identity securely. On POSIX, the identity directory must have owner-only permissions (`0700`) and the private key owner-only permissions (`0600`). Private identities and secret input files cannot be symlinks.
|
|
164
|
+
- A missing public identity can be regenerated from a valid private key. A corrupt private identity, or a missing private identity with an existing public key, is not silently replaced. Restore the identity or deliberately register a new one. Losing all authorized private keys makes existing ciphertext unrecoverable.
|
|
165
|
+
- Configre uses AES-256-GCM and RSA-3072/OAEP-SHA-256 for encryption and recipient key wrapping. Ciphertext is replaced atomically under a writer lock. If a stale lock is reported after a crash, confirm its writer has stopped before removing it; do not reset ciphertext or identities as a retry strategy.
|
|
166
|
+
|
|
167
|
+
## Print configuration without secret fields
|
|
168
|
+
|
|
169
|
+
Use `print()` instead of logging the merged config object, which contains decrypted secrets:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
const cfg = Configre(join(import.meta.dirname, "config"));
|
|
173
|
+
cfg.print();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`print()` calls Configre's LemonLog `log.debug`. Enable output with `DEBUG=Configre:*`; the `--debug` argument alone does not enable these logs. File creation, ciphertext updates, and public-key publication use `log.info`; authorization failures use `log.warn`, in the same namespace.
|
|
177
|
+
|
|
178
|
+
- `cfg.print()` prints current values, including edits made after loading, without modifying `cfg` or logging automatically on load.
|
|
179
|
+
- Fields present in loaded base or selected profile secret modules are omitted, including when read from ciphertext. Public sibling fields remain visible; arrays supplied by secrets are omitted entirely and emptied secret objects are removed.
|
|
180
|
+
- Classification is by secret-file fields, not names such as `password` or `token`. Sensitive values placed only in public settings or copied to another field are not automatically detected. Keep them in secret modules and do not treat `print()` as a general-purpose redactor.
|
|
181
|
+
- The method is non-enumerable: it does not appear in `Object.keys(cfg)`, object spreads, or JSON output. Those operations still include secret data, so do not use them as redaction. A spread or JSON round trip does not preserve the method.
|
|
182
|
+
- `new Configre(configPath).print()` is also supported and prints the instance's merged settings. `.get()` still returns plain configuration data. If a configuration already has its own `print` field, that value is preserved; use the constructor's `print()` method instead.
|
|
183
|
+
|
|
104
184
|
## Examples
|
|
105
185
|
|
|
106
186
|
**Example 1: Basic setup**
|
|
@@ -120,7 +200,7 @@ Result: staging overrides are merged over defaults, without conflicting with oth
|
|
|
120
200
|
|
|
121
201
|
## Key behaviors
|
|
122
202
|
|
|
123
|
-
- **Deep merge**: nested objects merge recursively
|
|
203
|
+
- **Deep merge**: nested plain objects merge recursively using Configre's own implementation; arrays merge by index and retain existing trailing entries rather than being replaced wholesale
|
|
124
204
|
- **Config files must use the `.cjs` extension** (`.js` is not accepted; works in both CommonJS and ESM projects)
|
|
125
205
|
- **Required path**: always pass the config directory or file path; prefer an absolute module-relative path over a process-relative path
|
|
126
|
-
- **Function vs constructor**: `Configre(path)` returns the merged config
|
|
206
|
+
- **Function vs constructor**: `Configre(path)` returns the merged config with non-enumerable `print()` when that field is available; `new Configre(path)` returns the instance with `.get()` and `.print()`
|
package/test/index.test.js
CHANGED
|
@@ -4,7 +4,65 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import test from "node:test";
|
|
7
|
-
import Configre from "../index.js";
|
|
7
|
+
import Configre, { applyConfigEnv } from "../index.js";
|
|
8
|
+
|
|
9
|
+
test("applyConfigEnv fills missing variables without overwriting existing values", t => {
|
|
10
|
+
const env = {
|
|
11
|
+
CONFIGRE_TEST_ENV_TEXT: "value",
|
|
12
|
+
CONFIGRE_TEST_ENV_NUMBER: 0,
|
|
13
|
+
CONFIGRE_TEST_ENV_BOOLEAN: false,
|
|
14
|
+
CONFIGRE_TEST_ENV_EMPTY: "",
|
|
15
|
+
CONFIGRE_TEST_ENV_NULL: null,
|
|
16
|
+
CONFIGRE_TEST_ENV_UNDEFINED: undefined,
|
|
17
|
+
CONFIGRE_TEST_ENV_EXISTING: "replacement",
|
|
18
|
+
CONFIGRE_TEST_ENV_EXISTING_EMPTY: "replacement"
|
|
19
|
+
};
|
|
20
|
+
const original = Object.fromEntries(Object.keys(env).map(key => [key, process.env[key]]));
|
|
21
|
+
t.after(() => {
|
|
22
|
+
for (const [key, value] of Object.entries(original)) {
|
|
23
|
+
if (value === undefined) delete process.env[key];
|
|
24
|
+
else process.env[key] = value;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
for (const key of Object.keys(env)) delete process.env[key];
|
|
28
|
+
process.env.CONFIGRE_TEST_ENV_EXISTING = "original";
|
|
29
|
+
process.env.CONFIGRE_TEST_ENV_EXISTING_EMPTY = "";
|
|
30
|
+
|
|
31
|
+
assert.equal(applyConfigEnv(env), undefined);
|
|
32
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_TEXT, "value");
|
|
33
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_NUMBER, "0");
|
|
34
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_BOOLEAN, "false");
|
|
35
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_EMPTY, "");
|
|
36
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_NULL, undefined);
|
|
37
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_UNDEFINED, undefined);
|
|
38
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_EXISTING, "original");
|
|
39
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_EXISTING_EMPTY, "");
|
|
40
|
+
applyConfigEnv({ CONFIGRE_TEST_ENV_TEXT: "replacement" });
|
|
41
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_TEXT, "value");
|
|
42
|
+
assert.equal(env.CONFIGRE_TEST_ENV_NUMBER, 0);
|
|
43
|
+
assert.equal(env.CONFIGRE_TEST_ENV_BOOLEAN, false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("applyConfigEnv accepts omitted configuration and rejects invalid inputs", () => {
|
|
47
|
+
assert.equal(applyConfigEnv(), undefined);
|
|
48
|
+
assert.equal(applyConfigEnv({}), undefined);
|
|
49
|
+
for (const env of [null, [], "text", 1, true, () => {}]) {
|
|
50
|
+
assert.throws(() => applyConfigEnv(env), {
|
|
51
|
+
name: "TypeError",
|
|
52
|
+
message: "config.env must be a plain object"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
assert.throws(() => applyConfigEnv({ "": null }), {
|
|
56
|
+
name: "TypeError",
|
|
57
|
+
message: "config.env keys must be non-empty strings"
|
|
58
|
+
});
|
|
59
|
+
for (const value of [{ nested: true }, [], new Date()]) {
|
|
60
|
+
assert.throws(() => applyConfigEnv({ CONFIGRE_TEST_ENV_INVALID: value }), {
|
|
61
|
+
name: "TypeError",
|
|
62
|
+
message: "config.env.CONFIGRE_TEST_ENV_INVALID must be a scalar"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
8
66
|
|
|
9
67
|
test("requires a config path", () => {
|
|
10
68
|
assert.throws(
|
|
@@ -44,17 +102,25 @@ for (const format of ["commonjs", "module"]) {
|
|
|
44
102
|
const entry = format === "commonjs" ? `
|
|
45
103
|
const assert = require('node:assert/strict');
|
|
46
104
|
const Configre = require('configre');
|
|
47
|
-
|
|
105
|
+
const { applyConfigEnv } = Configre;
|
|
106
|
+
import('configre').then(module => {
|
|
107
|
+
assert.equal(module.default, Configre);
|
|
108
|
+
assert.equal(module.applyConfigEnv, applyConfigEnv);
|
|
109
|
+
});
|
|
48
110
|
` : `
|
|
49
111
|
import assert from 'node:assert/strict';
|
|
50
112
|
import { createRequire } from 'node:module';
|
|
51
|
-
import Configre from 'configre';
|
|
113
|
+
import Configre, { applyConfigEnv } from 'configre';
|
|
52
114
|
const require = createRequire(import.meta.url);
|
|
53
115
|
assert.equal(require('configre'), Configre);
|
|
116
|
+
assert.equal(require('configre').applyConfigEnv, applyConfigEnv);
|
|
54
117
|
`;
|
|
55
118
|
const filename = path.join(directory, format === "commonjs" ? "consumer.cjs" : "consumer.mjs");
|
|
56
119
|
fs.writeFileSync(filename, entry + `
|
|
57
120
|
assert.equal(typeof Configre, 'function');
|
|
121
|
+
delete process.env.CONFIGRE_TEST_ENV_INTEROP;
|
|
122
|
+
applyConfigEnv({ CONFIGRE_TEST_ENV_INTEROP: 123 });
|
|
123
|
+
assert.equal(process.env.CONFIGRE_TEST_ENV_INTEROP, '123');
|
|
58
124
|
const configPath = ${JSON.stringify(config)};
|
|
59
125
|
const expected = { db: { host: 'localhost', port: 456 } };
|
|
60
126
|
assert.deepEqual(Configre(configPath), expected);
|