configre 2.1.2 → 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 +160 -197
- package/demo/config/hostname.cjs +0 -0
- package/demo/config/index.cjs +0 -0
- package/demo/secrets/config/index.cjs +3 -3
- package/demo/secrets/demo.js +1 -0
- package/index.js +61 -2
- package/package.json +6 -7
- package/skills/configre/SKILL.md +90 -10
- package/test/index.test.js +69 -3
- package/test/secrets.test.js +99 -0
package/README.md
CHANGED
|
@@ -1,134 +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.
|
|
11
|
+
```bash
|
|
12
|
+
npm install configre
|
|
13
|
+
```
|
|
32
14
|
|
|
33
|
-
|
|
34
|
-
- `[hostname].cjs`: Override configurations for specific hosts.
|
|
35
|
-
- `[hostname].dev.cjs`: Development-specific configurations.
|
|
15
|
+
Create `config/index.cjs` with your defaults:
|
|
36
16
|
|
|
37
|
-
|
|
17
|
+
```javascript
|
|
18
|
+
module.exports = {
|
|
19
|
+
db: {
|
|
20
|
+
host: "localhost",
|
|
21
|
+
port: 5432,
|
|
22
|
+
password: ""
|
|
23
|
+
},
|
|
24
|
+
api: {
|
|
25
|
+
key: ""
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
```
|
|
38
29
|
|
|
39
|
-
|
|
30
|
+
Load it from `app.mjs`, next to the `config` directory:
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
```javascript
|
|
33
|
+
import Configre from "configre";
|
|
34
|
+
import { join } from "node:path";
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
const cfg = Configre(join(import.meta.dirname, "config"));
|
|
37
|
+
console.log(cfg.db.port); // 5432
|
|
38
|
+
```
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
```bash
|
|
41
|
+
node app.mjs
|
|
42
|
+
```
|
|
50
43
|
|
|
51
|
-
|
|
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()`.
|
|
52
45
|
|
|
53
|
-
|
|
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.
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
const Configre = require("configre");
|
|
57
|
-
const { join } = require("node:path");
|
|
48
|
+
## Configuration that builds on defaults
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
```
|
|
50
|
+
An environment should describe what changes. Everything else comes from the base.
|
|
61
51
|
|
|
62
|
-
|
|
52
|
+
Add `config/production.cjs`:
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
```javascript
|
|
55
|
+
module.exports = {
|
|
56
|
+
db: {
|
|
57
|
+
host: "db.internal",
|
|
58
|
+
ssl: true
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
```
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
Select that profile:
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
```bash
|
|
66
|
+
node app.mjs --config=production
|
|
67
|
+
```
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
The resulting configuration contains:
|
|
72
70
|
|
|
73
71
|
```javascript
|
|
74
|
-
|
|
72
|
+
{
|
|
75
73
|
db: {
|
|
76
|
-
host:
|
|
77
|
-
|
|
78
|
-
password:
|
|
74
|
+
host: "db.internal",
|
|
75
|
+
port: 5432,
|
|
76
|
+
password: "",
|
|
77
|
+
ssl: true
|
|
79
78
|
},
|
|
80
79
|
api: {
|
|
81
|
-
key:
|
|
80
|
+
key: ""
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
```
|
|
85
84
|
|
|
86
|
-
|
|
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.
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
module.exports = {
|
|
90
|
-
db: {
|
|
91
|
-
user: 'john',
|
|
92
|
-
password: 'johns-password'
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
87
|
+
### Selecting a profile
|
|
96
88
|
|
|
97
|
-
Configre
|
|
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.
|
|
98
90
|
|
|
99
|
-
|
|
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 |
|
|
100
96
|
|
|
101
|
-
|
|
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`.
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
node demo.js --config=staging
|
|
105
|
-
node demo.js --config=production
|
|
106
|
-
node demo.js --port=3000 --config=myhost --debug
|
|
107
|
-
```
|
|
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"]`.
|
|
108
100
|
|
|
109
|
-
|
|
101
|
+
## Secrets, organized like your configuration
|
|
110
102
|
|
|
111
|
-
|
|
103
|
+
Database credentials belong under `db`. API keys belong under `api`. Secret files keep that organization while separating sensitive values from public settings.
|
|
112
104
|
|
|
113
|
-
|
|
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
|
|
111
|
+
```
|
|
114
112
|
|
|
115
|
-
|
|
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.
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
import Configre from "../../index.js";
|
|
119
|
-
import { join } from "node:path";
|
|
115
|
+
For `--config=production`, the merge order is:
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
```text
|
|
118
|
+
index.cjs → production.cjs → index.secret.cjs → production.secret.cjs
|
|
123
119
|
```
|
|
124
120
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
> The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets.
|
|
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.
|
|
128
122
|
|
|
129
|
-
###
|
|
123
|
+
### Add your secrets
|
|
130
124
|
|
|
131
|
-
|
|
125
|
+
Create `config/index.secret.cjs` for shared values:
|
|
132
126
|
|
|
133
127
|
```javascript
|
|
134
128
|
module.exports = {
|
|
@@ -138,175 +132,144 @@ module.exports = {
|
|
|
138
132
|
};
|
|
139
133
|
```
|
|
140
134
|
|
|
141
|
-
|
|
135
|
+
For a production database credential, create `config/production.secret.cjs`:
|
|
142
136
|
|
|
143
137
|
```javascript
|
|
144
|
-
module.exports = {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
node demo/secrets/demo.js
|
|
138
|
+
module.exports = {
|
|
139
|
+
db: {
|
|
140
|
+
password: ""
|
|
141
|
+
}
|
|
142
|
+
};
|
|
151
143
|
```
|
|
152
144
|
|
|
153
|
-
|
|
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.
|
|
154
146
|
|
|
155
|
-
|
|
156
|
-
demo/secrets/config/
|
|
157
|
-
index.cjs # Public configuration
|
|
158
|
-
index.secret.cjs # Editable secrets you create yourself
|
|
159
|
-
recipients/ # Public keys of servers that register
|
|
160
|
-
secrets.enc.json # Generated encrypted values
|
|
161
|
-
.gitignore # Keeps .secret.cjs files out of Git
|
|
162
|
-
```
|
|
147
|
+
### Share with a server
|
|
163
148
|
|
|
164
|
-
|
|
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`.
|
|
165
153
|
|
|
166
|
-
|
|
154
|
+
The server needs Git push access for registration. Until the updated encrypted file arrives, Configre loads public settings only.
|
|
167
155
|
|
|
168
|
-
|
|
169
|
-
API key configured: false
|
|
170
|
-
```
|
|
156
|
+
To update secrets later, edit them locally, run, and push. Pull and restart on the server. Registration happens only once per identity.
|
|
171
157
|
|
|
172
|
-
**
|
|
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.
|
|
173
159
|
|
|
174
|
-
|
|
160
|
+
## Utilities
|
|
175
161
|
|
|
176
|
-
###
|
|
162
|
+
### Inspect configuration with `cfg.print()`
|
|
177
163
|
|
|
178
|
-
|
|
164
|
+
The returned configuration contains decrypted secrets. Use `cfg.print()` when inspecting it:
|
|
179
165
|
|
|
180
166
|
```javascript
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
key: ""
|
|
184
|
-
}
|
|
185
|
-
};
|
|
167
|
+
const cfg = Configre(join(import.meta.dirname, "config"));
|
|
168
|
+
cfg.print();
|
|
186
169
|
```
|
|
187
170
|
|
|
188
|
-
|
|
171
|
+
Enable its debug output with:
|
|
189
172
|
|
|
190
173
|
```bash
|
|
191
|
-
node
|
|
174
|
+
DEBUG=Configre:* node app.mjs --config=production
|
|
192
175
|
```
|
|
193
176
|
|
|
194
|
-
|
|
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.
|
|
195
178
|
|
|
196
|
-
|
|
197
|
-
API key configured: true
|
|
198
|
-
```
|
|
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.
|
|
199
180
|
|
|
200
|
-
|
|
181
|
+
The constructor API is also available:
|
|
201
182
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
| `recipients/truco.pub` | Public identity of the server named `truco` | Yes; the server publishes it |
|
|
208
|
-
| `.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
|
+
```
|
|
209
188
|
|
|
210
|
-
|
|
189
|
+
If your configuration already has a field named `print`, Configre preserves it. Use the constructor API to print in that case.
|
|
211
190
|
|
|
212
|
-
|
|
191
|
+
### Apply environment variables explicitly
|
|
213
192
|
|
|
214
|
-
|
|
193
|
+
Use `applyConfigEnv(cfg.env)` when a library reads its settings from `process.env`:
|
|
215
194
|
|
|
216
|
-
```
|
|
217
|
-
|
|
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);
|
|
218
201
|
```
|
|
219
202
|
|
|
220
|
-
|
|
203
|
+
For CommonJS, use `const { applyConfigEnv } = require("configre")`.
|
|
221
204
|
|
|
222
|
-
|
|
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.
|
|
223
206
|
|
|
224
|
-
|
|
225
|
-
2. Writes `demo/secrets/config/recipients/truco.pub`.
|
|
226
|
-
3. Creates and pushes a commit containing only that public-key file.
|
|
227
|
-
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.
|
|
228
208
|
|
|
229
|
-
|
|
209
|
+
## Advanced reference
|
|
230
210
|
|
|
231
|
-
|
|
211
|
+
<details>
|
|
212
|
+
<summary>Secret formats, identities, Git behavior, and recovery</summary>
|
|
232
213
|
|
|
233
|
-
###
|
|
214
|
+
### Secret files and loading
|
|
234
215
|
|
|
235
|
-
|
|
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.
|
|
236
217
|
|
|
237
|
-
|
|
238
|
-
git pull --ff-only
|
|
239
|
-
node demo/secrets/demo.js
|
|
240
|
-
```
|
|
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.
|
|
241
219
|
|
|
242
|
-
|
|
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.
|
|
243
221
|
|
|
244
|
-
|
|
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.
|
|
245
223
|
|
|
246
|
-
|
|
247
|
-
git pull --ff-only
|
|
248
|
-
node demo/secrets/demo.js --config=truco
|
|
249
|
-
```
|
|
224
|
+
### Identities and registration
|
|
250
225
|
|
|
251
|
-
|
|
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.
|
|
252
227
|
|
|
253
|
-
|
|
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.
|
|
254
229
|
|
|
255
|
-
|
|
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.
|
|
256
231
|
|
|
257
|
-
|
|
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.
|
|
258
233
|
|
|
259
|
-
|
|
260
|
-
node demo/secrets/demo.js
|
|
261
|
-
```
|
|
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.
|
|
262
235
|
|
|
263
|
-
|
|
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.
|
|
264
237
|
|
|
265
|
-
|
|
238
|
+
### Updates, revocation, and recovery
|
|
266
239
|
|
|
267
|
-
|
|
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.
|
|
268
241
|
|
|
269
|
-
|
|
270
|
-
index.cjs → truco.cjs → index.secret.cjs → truco.secret.cjs
|
|
271
|
-
```
|
|
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.
|
|
272
243
|
|
|
273
|
-
|
|
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.
|
|
274
245
|
|
|
275
|
-
|
|
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.
|
|
276
247
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
- 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.
|
|
287
|
-
- 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.
|
|
288
|
-
- 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.
|
|
289
|
-
- 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.
|
|
290
|
-
- 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.
|
|
291
|
-
- 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.
|
|
292
|
-
- 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.
|
|
293
|
-
- 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.
|
|
294
257
|
|
|
295
258
|
</details>
|
|
296
259
|
|
|
297
|
-
##
|
|
260
|
+
## Resources and contributions
|
|
298
261
|
|
|
299
|
-
|
|
300
|
-
- **Clarity and Convenience**: Keep your configuration organized and easy to understand.
|
|
301
|
-
- **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).
|
|
302
263
|
|
|
303
|
-
|
|
264
|
+
To add the Configre skill to your coding agent:
|
|
304
265
|
|
|
305
|
-
|
|
266
|
+
```bash
|
|
267
|
+
npx skills add https://github.com/clasen/Configre --skill configre
|
|
268
|
+
```
|
|
306
269
|
|
|
307
|
-
|
|
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.
|
|
308
271
|
|
|
309
|
-
##
|
|
272
|
+
## License
|
|
310
273
|
|
|
311
274
|
The MIT License (MIT)
|
|
312
275
|
|
package/demo/config/hostname.cjs
CHANGED
|
File without changes
|
package/demo/config/index.cjs
CHANGED
|
File without changes
|
package/demo/secrets/demo.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
const configPath = join(import.meta.dirname, "config");
|
|
5
5
|
const cfg = Configre(configPath);
|
|
6
|
+
cfg.print();
|
|
6
7
|
console.info("API key configured:", Boolean(cfg.api.key));
|
|
7
8
|
console.info(`Set api.key in ${join(configPath, "index.secret.cjs")} and run this demo again.`);
|
|
8
9
|
console.info(`New servers automatically publish their public key in ${join(configPath, "recipients")}. Pull and rerun here to authorize them.`);
|
package/index.js
CHANGED
|
@@ -9,6 +9,52 @@ 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
|
+
|
|
36
|
+
function omitSecrets(settings, secrets) {
|
|
37
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
38
|
+
if (!Object.hasOwn(settings, key)) continue;
|
|
39
|
+
const current = settings[key];
|
|
40
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) &&
|
|
41
|
+
current !== null && typeof current === "object" && !Array.isArray(current)) {
|
|
42
|
+
omitSecrets(current, value);
|
|
43
|
+
if (Object.keys(current).length === 0) delete settings[key];
|
|
44
|
+
} else {
|
|
45
|
+
delete settings[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printSettings(settings, secretSettings) {
|
|
51
|
+
const output = merge({}, settings);
|
|
52
|
+
for (const secrets of secretSettings) {
|
|
53
|
+
omitSecrets(output, secrets);
|
|
54
|
+
}
|
|
55
|
+
log.debug(output);
|
|
56
|
+
}
|
|
57
|
+
|
|
12
58
|
class ConfigreClass {
|
|
13
59
|
constructor(pathOrDir) {
|
|
14
60
|
if (typeof pathOrDir !== "string" || pathOrDir.length === 0) {
|
|
@@ -83,6 +129,10 @@ class ConfigreClass {
|
|
|
83
129
|
get() {
|
|
84
130
|
return merge({}, this.defaultSettings, this.profileSettings, ...this.secretSettings);
|
|
85
131
|
}
|
|
132
|
+
|
|
133
|
+
print() {
|
|
134
|
+
printSettings(this.get(), this.secretSettings);
|
|
135
|
+
}
|
|
86
136
|
}
|
|
87
137
|
|
|
88
138
|
// Wrapper function to support both constructor and function usage
|
|
@@ -90,8 +140,17 @@ function Configre(path) {
|
|
|
90
140
|
if (this instanceof Configre) {
|
|
91
141
|
return new ConfigreClass(path);
|
|
92
142
|
} else {
|
|
93
|
-
|
|
143
|
+
const config = new ConfigreClass(path);
|
|
144
|
+
const settings = config.get();
|
|
145
|
+
if (!Object.hasOwn(settings, "print")) {
|
|
146
|
+
Object.defineProperty(settings, "print", {
|
|
147
|
+
value: () => printSettings(settings, config.secretSettings)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return settings;
|
|
94
151
|
}
|
|
95
152
|
}
|
|
96
153
|
|
|
97
|
-
|
|
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);
|
package/test/secrets.test.js
CHANGED
|
@@ -80,6 +80,105 @@ function consumerCopy(f, name) {
|
|
|
80
80
|
return { config };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
test("print logs public configuration without creating secret artifacts", t => {
|
|
84
|
+
const f = fixture(t, { seed: false });
|
|
85
|
+
const calls = [];
|
|
86
|
+
t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
|
|
87
|
+
const config = new Configre(f.config);
|
|
88
|
+
|
|
89
|
+
assert.equal(calls.length, 0);
|
|
90
|
+
config.print();
|
|
91
|
+
assert.deepEqual(calls, [[config.get()]]);
|
|
92
|
+
assert.equal(fs.existsSync(f.homes[0]), false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("function results expose print without changing enumerable data and log current settings", t => {
|
|
96
|
+
const f = fixture(t, { seed: false });
|
|
97
|
+
const calls = [];
|
|
98
|
+
t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
|
|
99
|
+
const cfg = Configre(f.config);
|
|
100
|
+
const expected = new Configre(f.config).get();
|
|
101
|
+
|
|
102
|
+
assert.equal(typeof cfg.print, "function");
|
|
103
|
+
assert.deepEqual(Object.keys(cfg), Object.keys(expected));
|
|
104
|
+
assert.deepEqual({ ...cfg }, expected);
|
|
105
|
+
assert.equal(JSON.stringify(cfg), JSON.stringify(expected));
|
|
106
|
+
cfg.api.host = "updated";
|
|
107
|
+
cfg.print();
|
|
108
|
+
assert.deepEqual(calls, [[{ ...expected, api: { key: "", host: "updated" } }]]);
|
|
109
|
+
assert.equal(cfg.api.host, "updated");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a public print field remains configuration data", t => {
|
|
113
|
+
const f = fixture(t, { seed: false });
|
|
114
|
+
fs.writeFileSync(path.join(f.config, "index.cjs"), 'module.exports = { print: false };');
|
|
115
|
+
assert.equal(Configre(f.config).print, false);
|
|
116
|
+
const calls = [];
|
|
117
|
+
t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
|
|
118
|
+
new Configre(f.config).print();
|
|
119
|
+
assert.equal(calls[0][0].print, false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("print omits local and encrypted secret fields without changing effective settings", t => {
|
|
123
|
+
const f = fixture(t);
|
|
124
|
+
const calls = [];
|
|
125
|
+
t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
|
|
126
|
+
writeSettings(f, {
|
|
127
|
+
api: { key: sentinel },
|
|
128
|
+
list: [sentinel],
|
|
129
|
+
privateGroup: { value: sentinel },
|
|
130
|
+
optional: null,
|
|
131
|
+
empty: "",
|
|
132
|
+
enabled: false,
|
|
133
|
+
count: 0
|
|
134
|
+
});
|
|
135
|
+
fs.writeFileSync(path.join(f.config, "testhost.secret.cjs"),
|
|
136
|
+
`module.exports = { api: { other: ${JSON.stringify(sentinel)} }, privateGroup: "public-looking" };`);
|
|
137
|
+
|
|
138
|
+
load(f);
|
|
139
|
+
for (const source of [f, consumerCopy(f, "debug-consumer")]) {
|
|
140
|
+
const config = new Configre(source.config);
|
|
141
|
+
const before = config.get();
|
|
142
|
+
const secretsBefore = structuredClone(config.secretSettings);
|
|
143
|
+
config.print();
|
|
144
|
+
|
|
145
|
+
assert.deepEqual(calls.at(-1), [{ api: { host: "profile" } }]);
|
|
146
|
+
assert.equal(JSON.stringify(calls).includes(sentinel), false);
|
|
147
|
+
assert.deepEqual(config.get(), before);
|
|
148
|
+
assert.deepEqual(config.secretSettings, secretsBefore);
|
|
149
|
+
assert.equal(before.api.key, sentinel);
|
|
150
|
+
assert.equal(before.list[0], sentinel);
|
|
151
|
+
assert.equal(before.list[1], 2);
|
|
152
|
+
const cfg = Configre(source.config);
|
|
153
|
+
cfg.api.key = sentinel + "-updated";
|
|
154
|
+
cfg.api.host = "updated";
|
|
155
|
+
cfg.print();
|
|
156
|
+
assert.deepEqual(calls.at(-1), [{ api: { host: "updated" } }]);
|
|
157
|
+
assert.equal(JSON.stringify(calls).includes(sentinel), false);
|
|
158
|
+
assert.equal(cfg.api.key, sentinel + "-updated");
|
|
159
|
+
}
|
|
160
|
+
const result = spawnSync(process.execPath, ["-e", `
|
|
161
|
+
const os = require('node:os');
|
|
162
|
+
os.homedir = () => process.env.CONFIGRE_TEST_HOME;
|
|
163
|
+
const Configre = require(process.env.CONFIGRE_TEST_MODULE);
|
|
164
|
+
Configre(process.env.CONFIGRE_TEST_PATH).print();
|
|
165
|
+
`, "--", "--config=testhost"], {
|
|
166
|
+
encoding: "utf8",
|
|
167
|
+
env: {
|
|
168
|
+
...process.env, DEBUG: "Configre:*",
|
|
169
|
+
CONFIGRE_TEST_HOME: f.homes[0],
|
|
170
|
+
CONFIGRE_TEST_MODULE: path.join(import.meta.dirname, "..", "index.js"),
|
|
171
|
+
CONFIGRE_TEST_PATH: path.join(f.root, "debug-consumer")
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
assert.equal(result.status, 0, result.stderr);
|
|
175
|
+
const output = result.stdout + result.stderr;
|
|
176
|
+
assert.match(output, /Configre:debug/);
|
|
177
|
+
assert.match(output, /host: 'profile'/);
|
|
178
|
+
assert.equal(output.includes(sentinel), false);
|
|
179
|
+
assert.equal(output.includes("privateGroup"), false);
|
|
180
|
+
});
|
|
181
|
+
|
|
83
182
|
function git(directory, args) {
|
|
84
183
|
const result = spawnSync("git", ["-C", directory, ...args], { encoding: "utf8" });
|
|
85
184
|
assert.equal(result.status, 0, result.stderr);
|