configre 2.1.2 → 2.1.4
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 +18 -1
- package/demo/secrets/demo.js +1 -0
- package/index.js +34 -1
- package/package.json +1 -1
- package/test/secrets.test.js +99 -0
package/README.md
CHANGED
|
@@ -124,7 +124,24 @@ const cfg = Configre(configPath);
|
|
|
124
124
|
|
|
125
125
|
Secrets activate automatically when the base configuration or selected profile has a corresponding `.secret.cjs` file, or when `secrets.enc.json` already exists. No options are needed. Loading remains synchronous, and your application reads the result through ordinary properties such as `cfg.api.key`.
|
|
126
126
|
|
|
127
|
-
> The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets.
|
|
127
|
+
> The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets. Use `cfg.print()` to log configuration with secret fields omitted.
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const cfg = Configre(configPath);
|
|
131
|
+
cfg.print();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`print()` calls Configre's `log.debug`; enable its output with `DEBUG=Configre:*`.
|
|
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.
|
|
128
145
|
|
|
129
146
|
### 1. Start the demo on the administrator machine
|
|
130
147
|
|
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,28 @@ import loadSecrets from "./secrets/index.js";
|
|
|
9
9
|
const requireConfig = createRequire(import.meta.url);
|
|
10
10
|
const log = lemonlog("Configre");
|
|
11
11
|
|
|
12
|
+
function omitSecrets(settings, secrets) {
|
|
13
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
14
|
+
if (!Object.hasOwn(settings, key)) continue;
|
|
15
|
+
const current = settings[key];
|
|
16
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) &&
|
|
17
|
+
current !== null && typeof current === "object" && !Array.isArray(current)) {
|
|
18
|
+
omitSecrets(current, value);
|
|
19
|
+
if (Object.keys(current).length === 0) delete settings[key];
|
|
20
|
+
} else {
|
|
21
|
+
delete settings[key];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printSettings(settings, secretSettings) {
|
|
27
|
+
const output = merge({}, settings);
|
|
28
|
+
for (const secrets of secretSettings) {
|
|
29
|
+
omitSecrets(output, secrets);
|
|
30
|
+
}
|
|
31
|
+
log.debug(output);
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
class ConfigreClass {
|
|
13
35
|
constructor(pathOrDir) {
|
|
14
36
|
if (typeof pathOrDir !== "string" || pathOrDir.length === 0) {
|
|
@@ -83,6 +105,10 @@ class ConfigreClass {
|
|
|
83
105
|
get() {
|
|
84
106
|
return merge({}, this.defaultSettings, this.profileSettings, ...this.secretSettings);
|
|
85
107
|
}
|
|
108
|
+
|
|
109
|
+
print() {
|
|
110
|
+
printSettings(this.get(), this.secretSettings);
|
|
111
|
+
}
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
// Wrapper function to support both constructor and function usage
|
|
@@ -90,7 +116,14 @@ function Configre(path) {
|
|
|
90
116
|
if (this instanceof Configre) {
|
|
91
117
|
return new ConfigreClass(path);
|
|
92
118
|
} else {
|
|
93
|
-
|
|
119
|
+
const config = new ConfigreClass(path);
|
|
120
|
+
const settings = config.get();
|
|
121
|
+
if (!Object.hasOwn(settings, "print")) {
|
|
122
|
+
Object.defineProperty(settings, "print", {
|
|
123
|
+
value: () => printSettings(settings, config.secretSettings)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return settings;
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
|
package/package.json
CHANGED
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);
|